C# meetings system - c#

I'm creating a form to hold information from "meetings". The user will fill out info regarding title, location, startTime, endTime, notes, and a date. What I am currently working on is the "save changes" button which will:
clear all the TextBoxes.
store the input in an array.
display only the title in the ListBox.
when the title is clicked on in the ListBox, the info stored in that array element re-populates in the appropriate TextBoxes should the user wish to make changes.
I have completed #1, #2 and #3 I would appreciate any help for #4. I've pasted the coding below for your viewing.
public partial class CalendarForm : Form
{
int currentIndex;
int arraySize = 0;
Meeting[] meetingArray = new Meeting[100];
public CalendarForm()
{
InitializeComponent();
}
private void saveChangesButton_Click(object sender, EventArgs e)
{
meetingArray[arraySize] = new Meeting();
meetingArray[arraySize].title = textBoxTitle.Text;
meetingArray[arraySize].location = textBoxLocation.Text;
meetingArray[arraySize].startTime = textBoxStartTime.Text;
meetingArray[arraySize].endTime = textBoxEndTime.Text;
meetingArray[arraySize].notes = notesTextBox.Text;
currentIndex = arraySize;
arraySize++;
meetingListBox.Enabled = true;
textBoxTitle.Text = "";
textBoxLocation.Text = "";
textBoxStartTime.Text = "";
textBoxEndTime.Text = "";
notesTextBox.Text = "";
*edit* added these two lines which now add the title to the listBox
meetingListBox.Items.Add(meetingArray[currentIndex].title);
Controls.Add(meetingListBox);
}
}
public class Meeting
{
public string title;
public string location;
public string startTime;
public string endTime;
public string notes;
};

This is how I would refactor the class:
public partial class CalendarForm : Form
{
private List<Meeting> Meetings { get; set; }
public CalendarForm()
{
InitializeComponent();
Meetings = new List<Meeting>();
}
private void saveChangesButton_Click(object sender, EventArgs e)
{
try
{
Meeting meeting = CreateMeeting();
Meetings.Add(meeting);
meetingListBox.Add(meeting);
}
catch
{
//Add proper error handling here
}
}
private Meeting CreateMeeting()
{
return new Meeting()
{
Title = textBoxTitle.Text,
Location = textBoxLocation.Text
StartTime = DateTime.Parse(textBoxStartTime.Text),
EndTime = DateTime.Parse(textBoxEndTime.Text),
Notes = notesTextBox.Text,
};
}
}
//As Matt Burland answered already:
private void meetingListBox_SelectedIndexChanged(object sender, EventArgs e)
{
Meeting meeting = meetingListBox.SelectedItem as Meeting;
if (meeting != null)
{
textBoxTitle.Text = meeting.Title;
//...etc for all your other text boxes.
}
}
public class Meeting
{
public string Title { get; set; }
public string Location { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public string Notes { get; set; }
public override string ToString()
{
return Title;
}
}
I've made a number of changes, more notably the switch from an Array to a List<>. Lists are more flexible and provide better functionality. Unless you really really need to use arrays, I would stay away from them just to better safeguard against logic errors index out of bounds type issues.
Also, I personally believe that dates should be stored in the DateTime struct format, but that is again a matter of preference. Note that it would be prudent to sanitize/validate the inputs (especially the dates) before assigning it into the Meeting object.
The Meeting object now has properties instead of public fields. Properties are preferred in case you ever want to change how something is Get/Set.
Hope this helps.

I really recommend you look up data binding and learn how to do this properly, but if you want a quick and dirty solution (although, in the end, you'll find it's a lot more work), I would do something like this:
private void saveChangesButton_Click(object sender, EventArgs e)
{
Meeting m = new Meeting();
m.title = textBoxTitle.Text;
m.location = textBoxLocation.Text;
m.startTime = textBoxStartTime.Text;
m.endTime = textBoxEndTime.Text;
m.notes = notesTextBox.Text;
meetingArray[arraySize] = m;
currentIndex = arraySize;
arraySize++;
meetingListBox.Enabled = true;
textBoxTitle.Text = "";
textBoxLocation.Text = "";
textBoxStartTime.Text = "";
textBoxEndTime.Text = "";
notesTextBox.Text = "";
meetingListBox.Items.Add(m);
//Controls.Add(meetingListBox); // You don't need to keep adding the control every time!
}
Now in your Meeting class, I'd override ToString() to just return the title. The ListBox will just use the ToString() method of whatever you add to it by default.
To help with #4, you want to bind the SelectedIndexChanged event and then use the SelectedItem property, cast it back to a Meeting object (because it'll return an Object) and then use it to repopulate your various text boxes.
Something like:
private void meetingListBox_SelectedIndexChanged(object sender, System.EventArgs e)
{
Meeting m = meetingListBox.SelectedItem as Meeting;
if (m != null)
{
textBoxTitle.Text = m.title;
//...etc for all your other text boxes.
}
}

Related

Prevent adding duplicate snack to ListView

I'm currently stuck on this for 2 hours and did everything I could to find answers but none yet!
public class Snack
{
public string snack { get; set; }
public double amount { get; set; }
public int price { get; set; }
public Snacks snacks;
public enum Snacks
{
friet,
kroket,
frikandel,
Burger,
}
}
In the load method, I'm initializing my ListView:
private void load()
{
lstSnacks.View = View.Details;
lstSnacks.Columns.Add("snack");
lstSnacks.Columns.Add("amount");
lstSnacks.Columns.Add("price");
cmbSnack.Items.AddRange(Enum.GetNames(typeof(Snack.Snacks)));
cmbSnack.SelectedIndex = (int)Snack.Snacks.friet;
}
After that, I have a button where I add some snacks with the amount entered in the textbox and a fixed price:
private void btnAdd_Click(object sender, EventArgs e)
{
if (!lstSnacks.Items.ContainsKey(cmbSnack.SelectedItem.ToString()))
{
double totalprice = 0;
Snack snack1 = new Snack();
snack1.snack = cmbSnack.SelectedItem.ToString(); ;
snack1.amount = Convert.ToDouble(txtAmount.Text);
ListViewItem item = lstSnacks.Items.Add(cmbSnack.SelectedItem.ToString());
item.SubItems.Add(snack1.amount.ToString());
if (cmbSnack.SelectedIndex == 0)
{
snack1.price = (int)(snack1.amount * 2.50);
item.SubItems.Add(snack1.price.ToString());
}
}
I need to look into my ListView and add a snack if it's not already there. If it is, it should tell you that it's already in there, but items.ContainsKey is not working for me properly. What am I doing wrong?
The line...
ListViewItem item = lstSnacks.Items.Add(cmbSnack.SelectedItem.ToString());
...sets the Text property of the ListViewItem. To be able to use ContainsKey(), you need to also set its Name property.
One more thing: Consider using double.TryParse() instead of Convert.ToDouble() because there's no guarantee that the user will enter a valid number. When dealing with user input, always favor the .TryParse() methods over Convert.ToXXXX() or .Parse().
I would change the code into something like this:
string itemName = cmbSnack.SelectedItem.ToString();
if (lstSnacks.Items.ContainsKey(itemName))
{
MessageBox.Show("The item already exists in the list.", "Duplicate item");
return;
}
if (!double.TryParse(txtAmount.Text, out double amount))
{
MessageBox.Show("Please enter a valid amount.", "Incorrect 'Amount' value");
return;
}
double totalprice = 0;
Snack snack1 = new Snack();
snack1.snack = itemName;
snack1.amount = amount;
ListViewItem item = lstSnacks.Items.Add(itemName);
item.Name = itemName;
item.SubItems.Add(snack1.amount.ToString());
if (cmbSnack.SelectedIndex == 0)
{
snack1.price = (int)(snack1.amount * 2.50);
item.SubItems.Add(snack1.price.ToString());
}

Return get set method value to label

In my Windows form I have 2 text boxes namely, start odometer reading and end odometer reading. My goal is to subtract the "start reading" from the "end reading" and display the difference in the label next to the Name and phone number of the client in the windows form label.
How do I return the value of the method getMilesCharge() and display it on the confirmLabel?
Code for the Car Rental Class
//A class that represents the Rental Agency Class.
namespace Assignment1
{
partial class RentalAgencyClass
{
//instance variables
public string customerName { get; set; }
public string phoneNumber { get; set; }
public double sMiles { get; set; }
public double eMiles { get; set; }
public double noOfDays { get; set; }
private double DAY_CHARGE = 15;
private double MILE_CHARGE = 0.12;
//Constructor class
//sets the value of the starting and ending miles.
//sets the value of the number of days the car was rented for
public RentalAgencyClass(double startMiles, double endMiles, double days)
{
startMiles = sMiles;
endMiles = eMiles;
days = noOfDays;
}
//method to calculate the number of miles driven on the rental
public double getMileCharge()
{
double milesDriven = 0;
milesDriven = eMiles - sMiles;
return milesDriven * MILE_CHARGE;
}
//method to calculate the Day Charges on the rental
public double getDayCharge()
{
return noOfDays * DAY_CHARGE;
}
//Property to display the information on the label
public string GetInfo()
{
return customerName + " | " + phoneNumber + " | " + getDayCharge() +" miles";
}
}
}
Form Designer Class code
namespace Assignment1
{
public partial class RentalAgencyClass : Form
{
RentalAgencyClass aCarRental;
public RentalAgencyClass()
{
InitializeComponent();
}
private void calculateButton_Click(object sender, EventArgs e)
{
try
{
//instantiates object
aCarRental = new RentalAgencyClass();
aCarRental.customerName = nameTextBox.Text;
aCarRental.phoneNumber = phoneTextBox.Text;
//aCarRental. = getDayCharge();
// aCarRental.milesDriven = //store the difference in this variable
//displayLabel.Text = "(student information saved)";
}
catch (Exception err)
{
MessageBox.Show(err.Message, "Error");
}
//Displays information about the Rental
confirmLabel.Text = aCarRental.GetInfo();
}
}
}
By calling aCarRental = new RentalAgencyClass(); within your calculateButton_Click method you are calling the parameterless constructor of your partial class RentalAgencyClass, which means in your case, you are creating a new instance of your form instead of setting your properties. So sMiles and eMiles will stay by their default value 0.
To get your code working you have to do several steps.
At first I recommend you should split your form and your agency class.
So let's say, rename your form class to RentalCalculator. As a next step you have to/can remove the partial from your RentalAgencyClass, because it is not a part of your form class anymore and I assume you did not want to extend your class in another part of your code.
As LarsTech pointed out in the comments. You should now fix your RentalAgencyClass constructor to:
public RentalAgencyClass(double startMiles, double endMiles, double days)
{
this.sMiles = startMiles;
this.eMiles = endMiles;
this.noOfDays = days;
}
and may add the following property to your class
public double milesDriven
{
get
{
return this.eMiles - this.sMiles;
}
}
At least you have to change your event handler:
private void calculateButton_Click(object sender, EventArgs e)
{
try
{
// if not existing you have to create some input textboxes
double startMiles = Convert.ToDouble(startMilesTextBox.Text);
double endMiles = Convert.ToDouble(endMilesTextBox.Text);
double days = Convert.ToDouble(daysTextBox.Text);
// Hint: you are creating a new instance on every button click
// and overwriting your field in your form class.
aCarRental = new RentalAgencyClass(startMiles, endMiles, days);
aCarRental.customerName = nameTextBox.Text;
aCarRental.phoneNumber = phoneTextBox.Text;
// Store the result in local variables
// if you want to do something with them later
double dayCharge = aCarRental.getDayCharge();
double milesCharge = aCarRental.getMilesCharge();
double drivenMiles = aCarRental.milesDriven;
// displayLabel.Text = "(student information saved)";
}
catch (Exception err)
{
MessageBox.Show(err.Message, "Error");
}
//Displays information about the Rental
confirmLabel.Text = aCarRental.GetInfo();
}
Answering your question:
How do I return the value of the method getMilesCharge() and display it on the confirmLabel?
You will have to change the following line in your calculateButton_Click method from:
confirmLabel.Text = aCarRental.GetInfo();
to:
confirmLabel.Text = aCarRental.getMilesCharge().ToString();
Last but not least let me give you a kind advice.
You may take a look at the Microsoft Naming Guidelines.
For example: Properties should be named in PascalCasing.
But this is just my personal opinion.

C# XML. Get data from XML to object, update in UI, save back to XML

let me describe the situation. Winforms C#
I have xml file with data. I load this data to an user defined class object using Deserialize.
Based on this object with data, I build [in Form] UI: many tabPages of custom controls (textBox, 2 buttons in groupBox). I can also save this user defined class object using Serialize to XML file.
Question:
When I update textBox.Text in Form UI in custom control I do not know how to keep connection with the object with data (Layout layout) and save the updated object with data to XML. The change of text happens only in user custom control TextBox. I want to update data from UI in data object (layout) and then simply save with Serialization.
user class:
public class Layout
{
public string type;
public List<TabPage> TabPageList;
public Layout()
{
this.TabPageList = new List<TabPage>();
}
}
public class TabPage
{
public string text;
public List<ActionGroup> ActionGroupList;
public TabPage()
{
this.ActionGroupList = new List<ActionGroup>();
}
}
public class ActionGroup
{
public string type;
public string text;
public string sourceLocal;
public string sourceRemote;
public ActionGroup()
{
this.type = string.Empty;
this.text = string.Empty;
this.sourceLocal = string.Empty;
this.sourceRemote = string.Empty;
}
}
Custom control:
public partial class ViewActionGroup : UserControl
{
public string type;
public string text;
public string sourceLocal;
public string sourceRemote;
public bool isRemote;
public bool isDone;
public ViewActionGroup()
{
this.type = string.Empty;
this.text = string.Empty;
this.sourceLocal = string.Empty;
this.sourceRemote = string.Empty;
this.isRemote = false;
this.isDone = false;
InitializeComponent();
}
public ViewActionGroup(ActionGroup actionGroup)
{
this.type = actionGroup.type;
this.text = actionGroup.text;
this.sourceLocal = actionGroup.sourceLocal;
this.sourceRemote = actionGroup.sourceRemote;
this.isRemote = false;
this.isDone = false;
InitializeComponent();
groupBox1.Text = text;
button1.Text = type;
button1.Click += new EventHandler(Button_Click);
textBox1.Text = sourceLocal;
textBox1.TextChanged += new EventHandler(textBox1_TextChanged);
}
public void ChangeToRemote()
{
isRemote = true;
textBox1.Text = this.sourceRemote;
}
public void ChangeToLocal()
{
isRemote = false;
textBox1.Text = this.sourceLocal;
}
private void textBox1_TextChanged(object sender, EventArgs e)
{
if (this.isRemote)
{
this.sourceRemote = textBox1.Text;
}
else
{
this.sourceLocal = textBox1.Text;
}
}
Creating UI where I loose connection between UI and data object:
private void CreateLayout(Layout layout)
{
this.Text = layout.type;
TabControl tabControl = new TabControl();
tabControl.Dock = DockStyle.Fill;
int tabCount = 0;
foreach (TabPage tabpage in layout.TabPageList)
{
int actionCount = 0;
tabControl.TabPages.Add(tabpage.text);
foreach (ActionGroup actionGroup in tabpage.ActionGroupList)
{
ViewActionGroup view = new ViewActionGroup(actionGroup);
view.Location = new Point(0, actionCount * view.Height);
tabControl.TabPages[tabCount].Controls.Add(view);
tabControl.TabPages[tabCount].AutoScroll = true;
tabControl.TabPages[tabCount].AutoScrollMinSize = new System.Drawing.Size(tabControl.Width/2,tabControl.Height);
actionCount++;
}
tabCount++;
this.panelMain.Controls.Add(tabControl);
}
}
There are two common ways:
One is a routine WriteDataIntoControls and another ReadDataFromControls where you transfer the data to and from your visible controls manually (advantage: highest degree of control). In this case you'd have to read your object from your XML source, deserialize it into your business object and create all visible controls together with their value. On saving you'd have to transfer all values into your business object and serizalize it after this.
The second is DataBinding (advantage: highest degree of automation). Read here: https://msdn.microsoft.com/en-us/library/ef2xyb33%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396
You can bind to simple values as well as to lists (including navigation) or complex objects.
You find a tutorial here: http://www.codeproject.com/Articles/24656/A-Detailed-Data-Binding-Tutorial
#Shnugo Thank You for your feedback. The tutorial you posted did not help because it is too hard for me but Data Binding topic gave me some clue.
Here easy tutorial in VB actually but simple. It helped me to do it quickly in C#.
https://www.youtube.com/watch?v=jqLQ2K9YY2A
C# solution
class MyObject
{
string name;
public MyObject()
{ }
public string Name
{
get { return name;}
set { name = value; }
}
}
public partial class Form1 : Form
{
MyObject obj = new MyObject();
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
obj.Name = "Lucas";
textBox1.DataBindings.Add("Text", obj, "Name", true, DataSourceUpdateMode.OnPropertyChanged);
}
private void button1_Click(object sender, EventArgs e)
{
label1.Text = obj.Name;
}
}

how do I get my method in eventdb.cs to populate my combobox in my main form?

I have a class called EventDB.cs that grabs data from a text file. I have every 5 lines input as a object list called events but am having trouble trying to populate the list into my combobox on my main form. Could someone point out what I'm doing wrong? Here is my code for my Event.cs, EventDB.cs and main ticketinfo.cs. Thanks in advance!
Event.cs
namespace TicketInformation
{
public class Event
{
public Event()
{
}
public Event(int day, string time, double price, string strEvent, string description)
{
this.Day = day;
this.Time = time;
this.Price = price;
this.StrEvent = strEvent;
this.Description = description;
}
public int Day { get; set; }
public string Time { get; set; }
public double Price { get; set; }
public string StrEvent { get; set; }
public string Description { get; set; }
public string GetDisplayText()
{
return StrEvent;
}
}
}
EventDB.cs
namespace TicketInformation
{
public static class EventDB
{
private static string dir = Directory.GetCurrentDirectory();
private static string path = dir + "\\calendar.txt";
public static List<Event> ExtractData() //(DateTime dtmDay)
{
//int intChosenDay = dtmDay.Day;
// create object for input stream for text file
StreamReader textIn =
new StreamReader(
new FileStream(path, FileMode.OpenOrCreate, FileAccess.Read));
//create the list
List<Event> events = new List<Event>();
string[] lines = File.ReadAllLines(path);
for (int index = 4; index < lines.Length; index += 5)
{
Event special = new Event();
special.Day = Convert.ToInt32(lines[index - 4]);
special.Time = (lines[index - 3]);
special.Price = Convert.ToDouble(lines[index - 2]);
special.StrEvent = lines[index - 1];
special.Description = lines[index];
events.Add(special);
}
//close stream for the text file
textIn.Close();
return events;
}
}
}
Ticketinfo.cs
static void Main()
{
Application.Run(new FrmEvents());
}
private List<Event> events = null;
private void FrmEvents_Load(
object sender, System.EventArgs e)
{
CreateEventList();
}
private void CreateEventList()
{
EventDB.ExtractData(events); //(mvwDate.SelectionStart);
cboEvent.Items.Clear();
foreach (Event e in events)
{
cboEvent.Items.Add(e.GetDisplayText());
}
} //end method CreateEventList
I think you make mistake in CreateEventList() method in Ticketinfo.cs. You dont update events in Ticketinfo.cs at all, you will have always empty list. You should change line
EventDB.ExtractData(events);
to
events = EventDB.ExtractData();
and then should work fine. now you method will return events from file.
You new methos should looks like :
private void CreateEventList()
{
events = EventDB.ExtractData(); //(mvwDate.SelectionStart);
cboEvent.Items.Clear();
foreach (Event e in events)
{
cboEvent.Items.Add(e.GetDisplayText());
}
} //end method CreateEventList
If you want to display events that are on or later than the selected date, there's a couple of things you can do:
First, you need an actual date in your Events class - you have a day and a time, but I don't see a date. So let's just use the Day property for purposes of this example.
You can bind your events list to your combo box based on the selected Day with LINQ very easily:
private void CreateEventList()
{
events = EventDB.ExtractData(); //(mvwDate.SelectionStart);
var e = (from ev in events
where ev.Day >= mvwDate.SelectionStart // mvwDate.SelectionStart needs to be an int
select ev).ToList();
cboEvent.Items.Clear();
cboEvent.DisplayMember = "StrDesc";
// You could also assign a ValueMember like this:
//cboEvent.ValueMember = "Day";
cboEvent.DataSource = e;
} //end method CreateEventList
There's a few assumptions in my example (mainly what mvwDate.SelectionStart is), and I haven't tested the code but this should give you another approach.

Store user input on selected day on month calendar

I have created a form that accepts user input for a couple different pieces of info (title, location , date(from a monthCalendar) etc..), when the Add button is clicked the information is stored in the current array element and the Title is displayed in a listBox. When the Title in the listBox is selected the rest of the information for that specific Title is re-populated in the appropriate textBox.
I have been trying to take this a step further with no success. When the Add button is clicked I would like to have the user input saved to the date selected on the monthCalendar. So if the user clicks on a date that has no information saved, the listBox remains empty. If there has been information saved on a date, the listBox would then display the Title(s).
Code snippets:
class MeetingManager
{
private Meeting[] meetings;
public int currentIndex;
public int maxIndex;
private string title;
private string location;
private string startTime;
private string endTime;
private string notes;
public MeetingManager()
{
meetings = new Meeting[10];
currentIndex = -1;
maxIndex = -1;
}
// excluded getter/setters + basic error checking
public void Add()
{
try
{
if (maxIndex >= meetings.Length - 1)
{
throw new ApplicationException("YOU CAN ONLY CREATE 10 MEETINGS");
}
else
{
maxIndex++;
currentIndex = maxIndex;
Meeting temp = new Meeting(Title, Location, StartTime, EndTime, Notes);
meetings[maxIndex] = temp;
Title = meetings[maxIndex].Title;
Location = meetings[maxIndex].Location;
StartTime = meetings[maxIndex].StartTime;
EndTime = meetings[maxIndex].EndTime;
Notes = meetings[maxIndex].Notes;
}
}
catch (ApplicationException ex)
{
throw ex; // toss it up to the presentation
}
}
public void Add(string title, string location, string startTime, string endTime, string notes)
{
try
{
Title = title;
Location = location;
StartTime = startTime;
EndTime = endTime;
Notes = notes;
Add();
}
catch (ApplicationException ex)
{
throw ex;
}
}
public override string ToString()
{
return Title;
}
}
public partial class CalendarForm : Form
{
private MeetingManager mManager; // reference to business layer object
private void calendarSaveChangesButton_Click(object sender, EventArgs e)
{
try
{
mManager.Title = textBoxTitle.Text;
mManager.Location = textBoxLocation.Text;
mManager.StartTime = maskedStartTimeTextBox.Text;
mManager.EndTime = maskedEndTimeTextBox.Text;
mManager.Notes = notesTextBox.Text;
mManager.Add();
meetingListBox.Enabled = true;
meetingListBox.Items.Add(mManager);
//clears the textBoxes after clickng saveChanges
textBoxTitle.Text = "";
textBoxLocation.Text = "";
maskedStartTimeTextBox.Text = "";
maskedEndTimeTextBox.Text = "";
notesTextBox.Text = "";
}
catch (ApplicationException ex)
{
MessageBox.Show(this, ex.Message);
}
}
/// <summary>
/// When a meeting is selected from the listBox, it re-populates
/// the empty fields with the information stored in the array element
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void meetingListBox_SelectedIndexChanged(object sender, EventArgs e)
{
MeetingManager m = meetingListBox.SelectedItem as MeetingManager;
if (m != null)
{
textBoxTitle.Text = m.Title;
textBoxLocation.Text = m.Location;
maskedStartTimeTextBox.Text = m.StartTime;
maskedEndTimeTextBox.Text = m.EndTime;
notesTextBox.Text = m.Notes;
}
}
}
Well, I think you could try something like this:
class MeetingManager
{
...
//add and implement a find method which returns a Meeting-object if there is a
//corresponding meeting date (in private Meeting[] meetings;)
public Meeting MeetingFinder(DateTime meetingTime)
{
//if there is a corresponding meeting-object for the date, return the meeting object
//if there isn't, return null
}
...
}
public partial class CalendarForm : Form
{
...
private void monthCalendar_DateChanged(object sender, DateRangeEventArgs e)
{
//which date was selected?
var selectedDate = monthCalendar.SelectionRange.Start;
//do we have that date in the meetings?
var meetingOnTheSelectedDate = mManager.MeetingFinder(selectedDate);
if(meetingOnTheSelectedDate != null)
{
//populate your winform with the data from meetingOnTheSelectedDate
}
}
...
}

Categories

Resources