Windows form takes time to respond - c#

I'm building a pretty big Winforms application. Everything worked fine, but in the past two days I'm have problems with times. For example, if I am running a loop that opens 8 tabs and creates a webbrowser in each, it takes it some time. The UI is kind of unresponsive while the function is running, but the big problem isn't in the creating.
I have a button that responsible for removing all the things I don't need from the UI(i.e. resetting it to the normal state). It just takes a huge time when there are about 10 tabs open. I measured the time and the time it takes the code to execute is about 1-1.2 seconds, but the time it takes to the UI to get responsive and preform everything I did is a lot more, about 10 seconds.
here is a code example:
private void killGUI()
{
DateTime a = DateTime.Now;
TimeSpan b;
this.SuspendLayout();
//tabPages.RemoveAll(TabPage);
tabPages.Clear();
if (tabControl1.TabPages.Count > 1)
{
//MessageBox.Show("" + tabControl1.TabPages.Count);
//DateTime a = DateTime.Now;
/*while (tabControl1.TabPages.Count != 1)
{
//int i = 1;
foreach (TabPage tab in tabControl1.TabPages)
{
if (tab.Name != "helpPanel")
{
tabControl1.TabPages.Remove(tab);
tab.Dispose();
}
}
}*/
while (tabControl1.TabPages.Count > 1)
{
Application.DoEvents();
TabPage t = tabControl1.TabPages[1];
tabControl1.TabPages.RemoveAt(1);
t.Dispose();
}
//TimeSpan v = DateTime.Now.Subtract(a);
//MessageBox.Show(""+v.Milliseconds);
}
///////
b = DateTime.Now.Subtract(a);
MessageBox.Show("REMOVING ALL TABS:" + a.Millisecond);
a = DateTime.Now;
////////
questions.ElementAt(0).richy.Dispose();
questions.ElementAt(0).createNewCom.Dispose();
//questions.ElementAt(questions.Count - 1).Name.Dispose();
for (int i = 0; i < questions.ElementAt(questions.Count - 1).comments.Count; i++)
{
Application.DoEvents();
if (questions.ElementAt(0).comments.ElementAt(i).texty != null)
questions.ElementAt(0).comments.ElementAt(i).texty.Dispose();
if (questions.ElementAt(0).comments.ElementAt(i).cButton != null)
questions.ElementAt(0).comments.ElementAt(i).cButton.Dispose();
Application.DoEvents();
}
/////
b = DateTime.Now.Subtract(a);
MessageBox.Show("REMOVING THIS QUESTION:" + a.Millisecond);
a = DateTime.Now;
/////
panel1.Visible = false;
while (panel2.Controls.Count != 0)
{
Application.DoEvents();
panel2.Controls.RemoveAt(0);
}
panel2.Visible = false;
backButton.Visible = false;
forwardButton.Visible = false;
//placePanel.Dispose();
//urgencyPanel.Dispose();
//categoriesPanel.Controls.Clear();
//categoriesPanel.Dispose();
//((Panel)((TabPage)tabControl1.Controls.Find("helpPanel", false)[0]).Controls.Find("placePanel", false)[0]).Dispose();
((Panel)((TabPage)tabControl1.Controls.Find("helpPanel", false)[0]).Controls.Find("categoriesPanel", false)[0]).Dispose();
((Panel)((TabPage)tabControl1.Controls.Find("helpPanel", false)[0]).Controls.Find("urgencyPanel", false)[0]).Dispose();
((Panel)((TabPage)tabControl1.Controls.Find("helpPanel", false)[0]).Controls.Find("placePanel", false)[0]).Dispose();
//Controls[] con=tabControl1.Controls.Find("HelpPanel",false);
newQuestionTextBox.Clear();
browsers.Clear();
panels.Clear();
buttons.Clear();
questions.RemoveAt(0);
finalTuid = "";
this.ResumeLayout();
foreach (Control cl in helpPanel.Controls)
{
Application.DoEvents();
if (cl.Name == "categoriesPanel" || cl.Name == "urgencyPanel" || cl.Name == "placePanel")
{
//WTF that shouldnt happen-i cant get this.
//MessageBox.Show("!!!");
cl.Dispose();
}
}
foreach (Control cl in helpPanel.Controls)
{
Application.DoEvents();
if (cl.Name == "categoriesPanel" || cl.Name == "urgencyPanel" || cl.Name == "placePanel")
{
//FFFFFFFFFFFUUUUUUUUUUUUUUUUUUU
//MessageBox.Show("!!!!!!!!");
cl.Dispose();
}
}
/////
b = DateTime.Now.Subtract(a);
MessageBox.Show("ALL ELSE:" + a.Millisecond);
///////
this.ResumeLayout();
}
also another probelm is if u can see:
((Panel)((TabPage)tabControl1.Controls.Find("helpPanel", false)[0]).Controls.Find("categoriesPanel", false)[0]).Dispose();
((Panel)((TabPage)tabControl1.Controls.Find("helpPanel", false)[0]).Controls.Find("urgencyPanel", false)[0]).Dispose();
((Panel)((TabPage)tabControl1.Controls.Find("helpPanel", false)[0]).Controls.Find("placePanel", false)[0]).Dispose();
This should remove three panels from the main panel, but it just doesn't work. I don't know why, but only after running the two loops below it removes the panels and just one loop isn't enough.
thanks so much in advance :)

Is there a reason why you don't have a reference to the help panel or the 3 other controls you want to dispose that exist in that panel? It seems really inefficient to have to iterate over the list of controls in your GUI (you said you had lots of tabs & controls) looking for these 3 specific controls. At bare minimum, you should cache the result of the helpPanel search as the next two lines are repeating the same search process that just occurred. The following loops can be solved with the same solution.
Wait, those two loops are iterating over helpPanel.Controls. So you already have a reference to the helpPanel and just not using it in the earlier steps?

Related

Hiding multiple columns programmatically in DataGridView is very slow

I have a DatagridView with about 300 columns and 80 rows.
Each column can be of 3 different types.
There are 3 check boxes which are responsible to show/hide the columns, each check box for each column type.
private void HideColumns(DataGridView datagridview)
{
if (datagridview.DataSource == null) return;
var watch = Stopwatch.StartNew();
// Added this code further the comment
Control c = datagridview;
while (c != this)
{
c.SuspendLayout();
c = c.Parent;
}
this.SuspendLayout();
CurrencyManager currencyManager = null;
try
{
RemoveHandler(datagridview); // remove all the handlers to the datagridivew for performance issue
currencyManager = (CurrencyManager)BindingContext[datagridview.DataSource];
currencyManager.SuspendBinding();
//datagridview.Visible = false;
for (int i = 0; i < datagridview.Columns.Count; i++)
{
var column = datagridview.Columns[i];
var itemType = datagridview.Rows[(int)OrdersAndComponentsRows.ItemType].Cells[column.Index].Value.ToString();
if (itemType == Glossary.IndirectCOType )
column.Visible = IndirectCOCheckBox.Checked;
else if (itemType == Glossary.NotAllocatedType )
column.Visible = NotAllocatedCheckBox.Checked;
else
column.Visible = DirectCOCheckBox.Checked;
}
}
finally
{
//datagridview.Visible = true;
if (currencyManager != null)
currencyManager.ResumeBinding();
AddHandler(datagridview);
// Added this code further the comment
c = datagridview;
while (c != this)
{
c.ResumeLayout();
c = c.Parent;
}
this.ResumeLayout();
}
// 3 check boxes are subscribed to this event
private void DisplayColumn_CheckedChanged(object sender, EventArgs e)
{
try
{
Cursor = Cursors.WaitCursor;
HideColumns(ShipCoverageDGV);
}
finally
{
Cursor = Cursors.Default;
}
}
The problem is when I am unchecking one of the checkboxes, it takes about 50 seconds to hide the columns.
The weird thing is that when checking the checkbox, it takes around 5 seconds to show the hidden columns.
Is there any operation that can be done in order to hide columns faster?
Setting AutoSizeRowMode to DisplayedHeaders solved the problem. It takes about 1 second.
Before setting the value, this property was set to AllCells
datagridview.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.DisplayedHeaders;
Note: Setting the property to None decrease the time to about 12 seconds.
The entire method including the fix:
private void HideColumns(DataGridView datagridview)
{
try
{
datagridview.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.DisplayedHeaders;
for (int i = 0; i < datagridview.Columns.Count; i++)
{
var column = datagridview.Columns[i];
var itemType = datagridview.Rows[(int)OrdersAndComponentsRows.ItemType].Cells[column.Index].Value.ToString();
if (itemType == Glossary.IndirectCOType)
column.Visible = IndirectCOCheckBox.Checked;
else if (itemType == Glossary.NotAllocatedType)
column.Visible = NotAllocatedCheckBox.Checked;
else
column.Visible = DirectCOCheckBox.Checked;
}
}
finally
{
datagridview.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.AllCells;
}
}
I think you need to figure out which part of the code is taking time.There are lots of things occurring in your hide columns method. Can you note down the time at the more granular level, like how much time been spent on suspending layout vs iterating over columns and making the columns hide.Once you know the problematic area you can work on optimizing that one.I am assuming here the problem statement is going to be the time that been spent on iteration as we have 300 columns and checking type of each one and hiding based on their type.

Issues with file splitting with c#

I've been trying to make a program to split larger text files into smaller pieces so they are easier to work with.
I currently have two issues and cannot figure out what is going on.
Issue 1: The background worker will sometimes fire multiple times. I cannot seem to figure out why or how many times it decides to run. It will run the split and on the final file it seems like it loops back to the beginning of do work and run it again. It fires off multiple do work completed tasks as well. To complicate things if I set the number of files split to a different number I can get a different number of times the background worker seems to fire, but it doesn't directly correlate to the number of files. Sometimes the same number of files causes the background worker to fire only once and sometimes it fires multiple times.
Issue 2: Sometimes the split will not create all the files. With some files if I run it it will create the first two files and then drop the rest. Seems to only happen when I set the number to 3 files to split into. if I take the line count and add it up it should equal out correctly. So i'm not sure what is going on there.
Calling Thread
private void StartSplit()
{
if (int.TryParse(NumberOfFilesTB.Text, out _numberOfFiles))
{
if (bg.IsBusy)
{
((MainWindow)Application.Current.MainWindow).SetStatus("Warning",
"Please only run one split process at a time.");
return;
}
((MainWindow)Application.Current.MainWindow).DisplayAlert(
"Split is running, you will receive an alert when it has finished. You may use other tools while the split is running.");
var args = new List<string> { _filepath, _includeHeaders.ToString(), _numberOfFiles.ToString() };
bg.DoWork += bg_DoWork;
bg.WorkerReportsProgress = true;
bg.ProgressChanged += ProgressChanged;
bg.RunWorkerCompleted += bg_RunWorkerCompleted;
bg.WorkerSupportsCancellation = true;
bg.RunWorkerAsync(args);
ProcessText.Text = "Running split process";
}
else
{
((MainWindow)Application.Current.MainWindow).SetStatus("Warning", "Please enter a number for number of files");
}
}
Background Thread
private void bg_DoWork(object sender, DoWorkEventArgs e)
{
var args = e.Argument as List<string>;
string filepath = args[0];
string includeHeaders = args[1];
int numberOfFiles = Convert.ToInt32(args[2]);
int numberOfRows = _lineCount / numberOfFiles;
_tempath = Path.GetDirectoryName(_filepath);
Directory.CreateDirectory(_tempath+"\\split");
if (includeHeaders == "True")
{
using (var reader = new StreamReader(File.OpenRead(filepath)))
{
_lines.Clear();
_header = reader.ReadLine();
_lines.Add(_header);
for (int i = 0; i < _lineCount; i++)
{
if (bg.CancellationPending)
{
e.Cancel = true;
break;
}
int percentage = (i + 1) * 100 / _lineCount;
bg.ReportProgress(percentage);
_lines.Add(reader.ReadLine());
if (i % numberOfRows == 0)
{
_counter++;
Debug.WriteLine(i);
if (i == 0)
{
//skip first iteration
_counter = 0;
continue;
}
_output = _tempath + "\\" + "split\\" + _fileNoExt + "_split-" + _counter + _fileExt;
_filesMade.Add(_output);
File.WriteAllLines(_output, _lines.ConvertAll(Convert.ToString));
_lines.Clear();
_lines.Add(_header);
}
}
}
}
else
{
using (var reader = new StreamReader(File.OpenRead(filepath)))
{
_lines.Clear();
_header = reader.ReadLine();
_lines.Add(_header);
for (int i = 0; i < _lineCount; i++)
{
if (bg.CancellationPending)
{
e.Cancel = true;
break;
}
int percentage = (i + 1) * 100 / _lineCount;
bg.ReportProgress(percentage);
_lines.Add(reader.ReadLine());
if (i % numberOfRows == 0)
{
_counter++;
if (i == 0)
{
//skip first iteration
_counter = 0;
continue;
}
string output = _tempath + "\\" + "split\\" + _fileNoExt + "_split-" + _counter + _fileExt;
_filesMade.Add(_output);
File.WriteAllLines(output, _lines.ConvertAll(Convert.ToString));
_lines.Clear();
}
}
}
}
}
Run Worker Completed
private void bg_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
StopSplit();
_filesMade.Clear();
ProcessText.Text = "Split cancelled";
return;
}
_filesMade.Clear();
ProcessText.Text = "Split has completed, click here to open the directory";
}
I bet your BgW is a member of your class...
In Startsplit() you add a new callback each time this function is executed.
That's why it runs multiple times.
Other answer after dinner.
Finished dinner...
your method of counting is faulty in multiple ways:
1) If you are missing files, I bet it's the last one. E.g. 30 lines, 3 files:
i % numberOfRows is zero at i=0, 10, 20, but i does not reach 30.
2) you are missing lines, e.g. 31 lines 4 files:
Files are saved at i=7, 14, 21, 28. Lines 29-31 are missing.
I suggest you use a nested for loop, outer one for files, inner one for lines, and improve your calculation. And put all your lists and counters inside the function!
I hope you appreciate my answer. I hate typing on a tablet. But also didn't want to start my computer just for this... ;-)

Locking thread in C#

In my C# project I m using the Timer.One(keyboard_timer) is for to watch if a user press F8 or not and another Timer(clipboard_time) is to watch if Clipboard contains a text or not.In my project The keyboard is always enable and the clipboard_timer is enabled when a user press F8.If the user again press F8 the clipboard_timer is disabled.what my project does that When user press F8 and he copies a word then my project show the meaning of the copied word in a window.my program runs on the background and always check if a user press F8 if he does then all-time my program check the clipboard, if it contains a text(word) if it does the show the meaning of the word everytime.
My code is here:
On the initialize
keyboard_timer.Enabled = true;
keyboard_timer.Tick += new EventHandler(keyboard_timer_Tick);
then
public void keyboard_timer_Tick(object sender, EventArgs e)
{
// clipboard enable
if ((a % 2) != 0)
{
// F9 is for Easy mood to eanble
if ((GetAsyncKeyState(Keys.F9) == -32767) && hot_key == "F9")
{
label2.Text = "Easy";
online_clipboard_active = "";
Clipboard.Clear();
clipboard_timer.Enabled = true;
clipboard_timer.Tick += new EventHandler(clipboard_timer_Tick);
++a;
}
///// // F8 is for online Mood
else if ((GetAsyncKeyState(Keys.F8) == -32767) && online_hot_key == "F8")
{
label2.Text = "Online";
online_clipboard_active = "on";
clipboard_timer.Enabled = true;
clipboard_timer.Tick += new EventHandler(clipboard_timer_Tick);
++a;
}
}// end of enable
//clipboard disable
if ((a % 2) == 0) //
{
// F9 is for Easy mood to disable here
if ((GetAsyncKeyState(Keys.F9) == -32767) && hot_key == "F9")
{
label2.Text = "Off";
clipboard_timer.Enabled = false;
++a;
}
// F8 is for online Mood to disable here
else if ((GetAsyncKeyState(Keys.F8) == -32767) && online_hot_key == "F8")
{
label2.Text = "Off";
online_clipboard_active = "";
clipboard_timer.Enabled = false;
++a;
}
}//end of clipboard disable
}// end of keyboard timer
Clipboard timer is
public void clipboard_timer_Tick(object sender, EventArgs e)
{
if (Clipboard.GetDataObject().GetDataPresent(DataFormats.Text))
{
string x = Clipboard.GetText();
Clipboard.Clear();
if ((a%2)==0 && online_clipboard_active == "on")
{
//cal online_mood form to translate the string from googletranslator
online_mood o = new online_mood(x);
o.Show();
}
else if((a%2)==0 && online_clipboard_active == "")
{
//cal show_meaning form to show the meaning into a window
show_meaning s = new show_meaning(x);
s.Show();
}
}
}// end of clipboard timer Tick
I want that when clipboard timer enable on that time keyboard timer will be lock because both of them uses a variable. When clipboard timer runs then keyboard timer will be lock and then when clipboard timer finished it works then the keyboard timer will be reactivated How can I solve this?
Anyone give me any help?????
Simple locking can be accomplish with a static member.
private static readonly object Sync = new object();
and then before setting or getting the info use
lock(Sync)
to lock the variable.
BTW: I don't get what you are trying to do. Maybe you should use the Keypress event of the form instead of the timers.
Look into C# locks
you basicly do this:
public Object lockObject = new Object(); //You want a separate object so that it's not changed when lock is active
...
//Code that can be run by any number of thread at the same time
...
lock(lockObject)
{
...
//Code that is accessable by only one thread at a time:
...
}
...
//Code that can be run by any number of thread at the same time
...
Hope this helps you out a bit.

Only execute method after X number of seconds since last use

I have a foreach statement, and I need to make it so that a method called at the end of the foreach and if statements only executes after 3 seconds from it's last execution time.
Here's the code.
//Go over Array for each id in allItems
foreach (int id in allItems)
{
if (offered > 0 && itemsAdded != (offered * 3) && tDown)
{
List<Inventory.Item> items = Trade.MyInventory.GetItemsByDefindex(id);
foreach (Inventory.Item item in items)
{
if (!Trade.myOfferedItems.ContainsValue(item.Id))
{
//Code to only execute if x seconds have passed since it last executed.
if (Trade.AddItem(item.Id))
{
itemsAdded++;
break;
}
//end code delay execution
}
}
}
}
And I don't want to Sleep it, since when an item is added, I need to get confirmation from the server that the item has been added.
How about a simple time comparison?
var lastExecution = DateTime.Now;
if((DateTime.Now - lastExecution).TotalSeconds >= 3)
...
You could save 'lastExecution' in your Trade-Class. Of course, the code block is not called (items are not added) with this solution if the 3 seconds haven't elapsed.
--//---------------------------------
Different solution with a timer: we add programmatically a Windows Forms timer component. But you will end in Programmers Hell if you use this solution ;-)
//declare the timer
private static System.Windows.Forms.Timer tmr = new System.Windows.Forms.Timer();
//adds the event and event handler (=the method that will be called)
//call this line only call once
tmr.Tick += new EventHandler(TimerEventProcessor);
//call the following line once (unless you want to change the time)
tmr.Interval = 3000; //sets timer to 3000 milliseconds = 3 seconds
//call this line every time you need a 'timeout'
tmr.Start(); //start timer
//called by timer
private static void TimerEventProcessor(Object myObject, EventArgs myEventArgs)
{
Console.WriteLine("3 seconds elapsed. disabling timer");
tmr.Stop(); //disable timer
}
DateTime? lastCallDate = null;
foreach (int id in allItems)
{
if (offered > 0 && itemsAdded != (offered * 3) && tDown)
{
List<Inventory.Item> items = Trade.MyInventory.GetItemsByDefindex(id);
foreach (Inventory.Item item in items)
{
if (!Trade.myOfferedItems.ContainsValue(item.Id))
{
//execute if 3 seconds have passed since it last execution...
bool wasExecuted = false;
while (!wasExecuted)
{
if (lastCallDate == null || lastCallDate.Value.AddSeconds(3) < DateTime.Now)
{
lastCallDate = DateTime.Now;
if (Trade.AddItem(item.Id))
{
itemsAdded++;
break;
}
wasExecuted = true;
}
System.Threading.Thread.Sleep(100);
}
}
}
}
}

In my numericUpDown Changed event im calling a function() and when im changing the numeric value when program is running its getting slower

This is the numeric changed event code with timer2 wich didnt solve hte problem the function im calling is DoThresholdCheck()
The problem is that in this function im creating each time im changing the numeric value a temp list each time im moving the numeric value change it the list is been created from the start. The problem is that if im using a big file in my program the list is containing sometimes 16500 indexs and its taking time to loop over the list so i guess when im changing the numeric value its taking time to loop over the list. If im using smaller video file for example the list containing 4000 indexs so there is no problems. I tried to use Timer2 maybe i could wait 0.5 seconds between each time the numeric value is changed but still dosent work good.
When im changing the numeric value while the program is running on a big video file its taking the values to be changed like 1-2 seconds ! thats a lot of time.
Any way to solve it ? Maybe somehow to read the list loop over the list faster even if the list is big ?
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
Options_DB.Set_numericUpDownValue(numericUpDown1.Value);
if (isNumericChanged == true)
{
isNumericChanged = false;
myTrackPanelss1.trackBar1.Scroll -= new EventHandler(trackBar1_Scroll);
DoThresholdCheck();
counter = 0;
}
}
private void timer2_Tick(object sender, EventArgs e)
{
counter++;
if (counter > 1)
{
isNumericChanged = true;
//timer2.Stop();
}
}
This is the DoThresholdChecks() function code:
private void DoThresholdCheck()
{
List<string> fts;
//const string D6 = "000{0}.bmp";
if (Directory.Exists(subDirectoryName))
{
if (!File.Exists(subDirectoryName + "\\" + averagesListTextFile + ".txt"))
{
return;
}
else
{
bool trackbarTrueFalse = false ;
fts = new List<string>();
int counter = 0;
double thershold = (double)numericUpDown1.Value;
double max_min_threshold = (thershold / 100) * (max - min) + min;
//label13.Text = max_min_threshold.ToString();
_fi = new DirectoryInfo(subDirectoryName).GetFiles("*.bmp");
for (int i = 0; i < myNumbers.Count; i++)
{
if (myNumbers[i] >= max_min_threshold)
{
//f.Add(i);
string t = i.ToString("D6") + ".bmp";
if (File.Exists(subDirectoryName + "\\" + t))
{
counter++;
button1.Enabled = false;
myTrackPanelss1.trackBar1.Enabled = true;
trackbarTrueFalse = true;
label9.Visible = true;
// myTrackPanelss1.trackBar1.Scroll += new EventHandler(trackBar1_Scroll);
//myTrackPanelss1.trackBar1.Minimum = 0;
// myTrackPanelss1.trackBar1.Maximum = f.Count;
// myTrackPanelss1.trackBar1.Value = f.Count;
// myFiles = new Bitmap(myTrackPanelss1.trackBar1.Value);
}
else
{
label9.Visible = false;
trackbarTrueFalse = false;
button1.Enabled = true;
myTrackPanelss1.trackBar1.Enabled = false;
myTrackPanelss1.trackBar1.Value = 0;
pictureBox1.Image = Properties.Resources.Weather_Michmoret;
label5.Visible = true;
secondPass = true;
break;
}
//fts.Add(string.Format(D6, myNumbers[i]));
}
}
//myTrackPanelss1.trackBar1.Maximum = _fi.Length - 1;
if (myTrackPanelss1.trackBar1.Maximum > 0)
{
if (trackbarTrueFalse == false)
{
myTrackPanelss1.trackBar1.Value = 0;
}
else
{
myTrackPanelss1.trackBar1.Maximum = counter;
myTrackPanelss1.trackBar1.Value = 0;
SetPicture(0);
myTrackPanelss1.trackBar1.Scroll += new EventHandler(trackBar1_Scroll);
}
//checkBox2.Enabled = true;
}
if (_fi.Length >= 0)
{
label15.Text = _fi.Length.ToString();
label15.Visible = true;
}
}
}
else
{
button1.Enabled = true;
}
}
try to cache results from DoThresholdCheck method
You can't magically get around the time the processing takes, if the processing is really necessary. You've got a couple of avenues to explore:
1) Minimise the processing being done - is all of it necessary? Can you cache any of it and only recompute a small amount each time the value changes?
2) Do the processing less often - do you have to recompute every single time?
3) Do the processing on another thread - then at least your UI remains responsive, and you can deliver the results to the UI thread when the background task is complete. However, this is going to be a relatively complicated variant of this pattern as you're going to need to be able to abort and restart if the value changes again while you're still processing the previous one.

Categories

Resources