Using large number of UserControl in WindowsForms - c#

I'm used to working with HTML apps in which there is no problem of creating a list of components (like phonebook contact).
Example of my app
Now I have used this method in Windows Forms. One panel is populated with large number of custom UserControls (UC). Only 30 UserControls takes more than 5s to render. While the list of data from database is returnd in <1s.
This UC has only labels, PictureBox and Click event. It's called like this. This data is used to populate child controls.
new UserControl(MyModel data);
Is there a beter way of doing this? I would like to have user friendly GUI and not using Grid layout. It's not user friendly and very limited in terms of how data can be showed to user.
foreach (var data in myDbResult)
{
var uc = new MyUserControl(data);
uc.Dock = DockStyle.Top;
resultFlowPanel.Controls.Add(uc);
}
...
public MyUserControl(MyModel data)
{
this.data = data;
InitializeComponent();
label1.Text = data.name;
label2.Text = data.address;
// get some more data from database
using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["AlarmDbCon"].ConnectionString))
{
payer = db.Query<Models.g_UserPayer>("SELECT TOP(1) * FROM g_UserPayer WHERE ID_User=#UserID", new
{
UserID = guard.ID_User
}).FirstOrDefault();
}
label3.Text = payer.email;
PictureBox.Image = payer.image;
}

If there are your own UI controls then you should consider using in your Load method the this.SuspendLayout(); and this.ResumeLayout(); methods to postpone the expensive UI layout calculation of the UI control.
Another aspect you can use is to load your expensive data on a Background Thread. To be able to do this you should use delegates to be able to update controls created on the UI Thread. More info on this on this SO answer.

Related

Best way to resolve OutOfMemoryException? (C#)

I'm using C#, .Net 4.6.x, and the Telerik controls for WinForms in Visual Studio 2017 RC. The application consists of a "main" window that uses a RadRibbon and a RadPageView. The RadPageView is dynamically populated with pages based on a search form which is the first page, or by a user requesting a new blank form. The other pages are inherited from RadPageViewPage and have an additional property called "TQC". TQC refers to a custom control that loads on the page.
The TQC has several dropdownlists and text areas in RadPageViewPages that are all contained in a RadPageView control within the TQC object. It doesn't bind any data to its controls until its RPVP (the class inheriting RadPageViewPage) is Selected. One of the dropdownlists contains 200 or so entries when it's populated (a list of accounts).
The problem I've run into is that when the outer RadPageView Remove()s a page, the memory taken by that page isn't freeing up, to the order of a couple hundred megabytes. This is problematic, as the target machines have between 4GB and 8GB of RAM. I tried setting the data objects that populate the controls to null as part of the closing event, but nothing changed. I also tried explicitly calling the Dispose() method on all descendants of the RadPageViewPage as below:
private void rpvQtabs_PageRemoved(object sender, RadPageViewEventArgs e)
{
foreach (TQC c in e.Page.Controls)
{
foreach (Control ca in c.Controls)
{
foreach (Control cac in ca.Controls)
{
cac.Dispose();
}
ca.Dispose();
}
c.Dispose();
}
e.Page.Dispose();
}
I'm still getting a crazy huge memory leak, and if more than 5 tabs are viewed by the user (even if they close the page), an OutOfMemoryException is soon to follow. I tried attaching the Performance Profiler, but it crashes on the attempt. VS 2015 isn't an option at the moment. How can I ensure that the pages are disposed properly, or reduce the memory footprint on the extremely large dropdownlists? This is our first foray into using Telerik.
In response to questions in the comments:
The object that throws the error is usually relatively random and depends on which accounts are loaded. It's not anything infinitely recursive. This is how the control's is originally loaded (using a special connection class):
public static List<Account> List(bool includeDefaults = true)
{
//search
var rs = new List<Account>();
string q = "select distinct r.ID, r.name from db.addressbook r";
DataTable dt = new DataTable();
using (var cmd = new CustomConnectionClass())
{
cmd.Safety.Off();
dt = cmd.ExecuteDirectQuery(q);
}
foreach (DataRow r in dt.Rows)
{
var a = new Account();
a.ID = long.Parse(r[0].ToString());
a.Name = r[1].ToString();
rs.Add(a);
}
rs = rs.OrderBy(t => t.ID).ToList();
var n = new Account();
n.ID = 0;
n.Name = "Generic Account";
var o = new Account();
o.ID = 999999;
o.Name = n.Name;
rs.InsertRange(0, new Account[] { n, o });
return rs;
}
After further investigation, I was able to sort out that this involves how Telerik loads its themes and controls. Making all datasources for the dropdownlists static reduced the footprint a bit, but the thememanager and themes for the controls within each tab were loaded as new each time. By design, they aren't necessarily disposed when the control is closed and disposed. The design of the UI will have to be re-worked to prevent users from running out of memory. The problem goes away if the same design is incorporated using standard WinForms.

Should code be inside user control or the form using it

I have a small design question which I couldn't find relevant google hits for some reason.
I have a user control which I use in my application.
The main form opens a second form as a dialog. T
his second form is using the user control which includes a list box.
Naturally I want to preserve the list box items when the forms dispose so I am keeping a private list in the main form.
List<string> _listofFirstCoordinates = new List<string>();
Now the question is, should the dialog form be the one responsible for relaying the list to the main form or should the code be in the user control?
Should the one populating the list be the user control
lst_Coordinates.Items.AddRange(ListOfCoordinates.Cast<object>().ToArray());
or should the form using it populate it (The subform)
uc_EditCoordinates.ListOfCoordinates = ListOfCoordinates;
Also is it feasible to just have the user control be a public variable for the form holding it so it may be changed directly or would that be bad design?
Edit:
By the way, the data is saved for now in variables going back and forth between the forms as the user has to finish all subforms before submitting and finally saving it to the database. So it is a
var _listofFirstCoordinates = new List<string>();
going back and forth.
The "correct" solution is to abstract-away the View-level concern (in this case, anything to do with Form, UserControl, and UI controls) away from the Controller and Model-level concerns (in this case, your application's data).
Without completely rearchitecturing your system, you can still apply this separation-of-concerns within your example.
You can conceptually argue the "code-behind" of your MainForm class acts as a kind of Controller (purists would disagree). It will have to know about creating the child form, but it does not need to know about the user-control hosted within the child form - that would be the concern of the child form's.
I suggest defining a class that represents a ViewModel - albeit as we're using WinForms we will use it as a kind of crude "one-way" ViewModel, like so:
class MainForm : Form {
private void ShowChildFormModal() {
ChildViewModel vm = new ChildViewModel();
vm.CoordinatesList = ...
vm.OtherData = ...
ChildForm child = new ChildForm();
child.LoadFromViewModel( vm );
child.ShowDialog();
child.SaveToViewModel( vm );
SaveToDatabase( vm );
}
}
class ChildViewModel { // this is a POCO
public List<String> CoordinatesList;
public Int32 OtherData;
}
class ChildForm : Form {
public void LoadFromViewModel(ChildViewModel vm) {
// save time and trouble by using the List as a datasource directly, or you can manually populate the combobox as well
this.childUserControl.LoadFromViewModel( vm );
this.someOtherControl.Value = vm.OtherData;
}
public void SaveToViewModel(ChildViewModel vm) {
// completing this is an exercise for the reader
// but basically copy values from the controls on the form into the `vm` instance
}
}
class ChildUserControl : UserControl {
public void LoadFromViewModel(ChildViewModel vm) {
this.comboBox.DataSource = vm.CoordinatesList;
}
}

Attach new Timer to every Object

I am using WPF and created a Window with informations about the computer.
It stores informations like Network connectivity, IP's, Subnet masks, Network devices and other stuff.
To track changes in the system I want to add an timer on an object to refresh itself. I don't want to refresh the hole form because I had HttpWebRequests in it and it will freeze the programm for a few seconds. It should be easier to see changes and to highlight them.
For example:
StComputerInf.Children.Add(new Label { Content = "2. Domain: \t\t" + System.Environment.UserDomainName });
I want to add here an timer to refresh itself.
And for every TreeViewItem in a TreeView:
public TreeView CreatTVConnection()
{
List<CAdapter> LAdapter = new List<CAdapter>();
List<TreeViewItem> lConnectedDevices = new List<TreeViewItem>();
List<TreeViewItem> lDisconnectedDevices = new List<TreeViewItem>();
LAdapter = ReadAdapter();
TreeView tv_Adapter = new TreeView();
tv_Adapter.Name = "Adapter";
tv_Adapter.Background = System.Windows.Media.Brushes.Transparent;
tv_Adapter.BorderThickness = new Thickness(0);
TreeViewItem Connected = new TreeViewItem();
TreeViewItem Disconnected = new TreeViewItem();
lConnectedDevices = LoadTV(true, LAdapter);
if (lConnectedDevices.Count > 0)
{
Connected.Header = "Connected:";
Connected.FontWeight = FontWeights.Bold;
Connected.Foreground = System.Windows.Media.Brushes.Green;
Connected.Name = "Connected";
foreach (TreeViewItem tvi in lConnectedDevices)
{
tvi.FontWeight = FontWeights.Normal;
Connected.Items.Add(tvi);
}
}
....
And is there a way to see if an object have changed? So I can highlight the affected object?
Use a factory to create your objects.
So for the label example you'd use something like
LabelFactory.Create(any useful parameters here) and as part of that method you can include a timer etc.
Also, look into using async/await to update your forms as an easier way to update them without freezing the forms. Once you are comfortable with the pattern you should be able to remove the dependency on timers.

Sharing a Binding Source Between Two Controls Not Working

I'm creating a form that will act as a master-detail editor. I have 2 controls on this form. One is basically a list of mater items and the other control is the details of the item. I'm trying to use a the same BindingSource object in both controls so that when a change is made on the master control, the detail control will get updated.
In my form I have:
EmployerCollection employerCollection = new EmployerCollection();
employerCollection.GetMulti(null, 0, new SortExpression(EmployerFields.Name | SortOperator.Ascending));
bsEmployers.DataSource = employerCollection;
masterControl.Init(bsEmployers);
detailControl.Init(bsEmpoyers);
In my masterControl I have:
public void Init(BindingSource bs)
{
bsEmployers = bs;
}
However for the life of me I can't get my master control to display the data in the binding source when I pass it in this way.
I can get binding to work only if I remove the bsEmployers = bs line and move the other logic as follows:
public void Init(BindingSource bs)
{
EmployerCollection employerCollection = new EmployerCollection();
employerCollection.GetMulti(null, 0, new SortExpression(EmployerFields.Name | SortOperator.Ascending));
bsEmployers.DataSource = employerCollection;
}
Does anyone have any idea what I can't pass the BindingSource object in to share it? I tried calling RefreshBindings in my control, but it did not seem to have any effect.
Thanks.
I don't think that the BindingSource was intended to be used that way. I would recommend passing the EmployerCollection value to the Init method and have each control create its own BindingSource using the EmployerCollection.

.NET 3.5 WinForms - DataGridView crashes on refresh(). Is it a bug?

This is a multi threaded scenario.
The main thread handles the application and UI events, and it starts up a new thread to do some background operations.
The "background" thread loads the data from files into a data-table of a strongly-typed dataset. The DataGridView is bound to that DataTable.
Once the data is ready, the "background" thread invokes the refresh() function of the DataGridView on the form.
If there are more lines then what fits on one screen and the vertical scrollbar is to appear: the grid crashes. The new datalines are always displayed. Error only occurs if there are enough lines to display the scrollbar (see image below).
I use .NET 3.5. In Windows XP it crashes the whole application. On Win 7 (64 bit) only the grid becomes unresponsive, but once I resize the window the scrollbar appears and all is fine.
The relevant parts of the code are attached below.
Grid refresh operation in the form's .cs file:
public void ThreadSafeRebindGrids()
{
SimpleCallBack callBackHandler = new SimpleCallBack(RebindGrids);
this.BeginInvoke(callBackHandler);
}
public void RebindGrids()
{
gridCurrentResults.Refresh(); // The problematic DataGridView refresh()
gridAllResults.Refresh();
}
public delegate void SimpleCallBack();
The update part in the "background" thread:
void Maestro32_SampleFinished(object sender, MeasurementEvents.SampleFinishedEventArgs e)
{
//--- Read new results
ParentForm.ThreadSafeSetStatusInfo("Processing results for sample no. " + e.SampleNo.ToString() + "...");
CurrentMeasurement.ReadSpeResults(); // Updating the DataTable in the strongly typed DataSet (see below)
ParentForm.ThreadSafeRebindGrids(); // Refresh the DataGridView
ParentForm.ThreadSafeRefreshNumbers();
}
The objects related to the "background" thread have a direct reference to the DataSet (UiDataSource). The DataTable (CurrentSamples) is updated in the following manner:
/// <summary>
/// Adds a new sample to the CurrentSamples table of the UiDataSet.
/// </summary>
/// <param name="sample">The new sample to be added to the table.</param>
/// <param name="serial">The serial number of the sample being added</param>
private void AddSampleToCurrentResults(SampleData sample, int serial)
{
UiDataSource.CurrentSamples.AddCurrentSamplesRow(serial,
sample.MeasurementDate,
(uint)Math.Round(sample.SampleCountSum),
true, //--- Set the checkbox checked
sample.LiveTime,
sample.RealTime);
}
DataGridView options:
//
// gridCurrentResults (generated)
//
this.gridCurrentResults.AllowUserToAddRows = false;
this.gridCurrentResults.AllowUserToDeleteRows = false;
this.gridCurrentResults.AllowUserToOrderColumns = true;
this.gridCurrentResults.AllowUserToResizeRows = false;
this.gridCurrentResults.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.gridCurrentResults.AutoGenerateColumns = false;
this.gridCurrentResults.CausesValidation = false;
this.gridCurrentResults.ColumnHeadersHeight = 25;
this.gridCurrentResults.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.selectedCol,
this.SampleNoCol,
this.MeasuredValueCol,
this.liveTimeCol,
this.realTimeDataGridViewTextBoxColumn,
this.AtTimeCol});
this.gridCurrentResults.DataMember = "CurrentSamples";
this.gridCurrentResults.DataSource = this.uiDataSource;
this.gridCurrentResults.Location = new System.Drawing.Point(11, 24);
this.gridCurrentResults.Margin = new System.Windows.Forms.Padding(8);
this.gridCurrentResults.Name = "gridCurrentResults";
this.gridCurrentResults.RowHeadersVisible = false;
this.gridCurrentResults.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.gridCurrentResults.ShowEditingIcon = false;
this.gridCurrentResults.Size = new System.Drawing.Size(534, 264);
this.gridCurrentResults.TabIndex = 0;
this.gridCurrentResults.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.gridCurrentResults_CellContentClick);
If I made a mistake somewhere please point it out to me.
#ChrisF:
I tried removing the refresh() statement, as I am doing pretty much the same what u suggested. The only difference is the databinding, it looks like:
this.dataGridView.DataSource = this.dataSet;
this.dataGridView.DataMember = "dataTable";
And I update the dataTable in a similar way, but from another thread.
But the new data lines do not appear until I, say, resize the window.
Which raises the question how I can properly update the dataTable from another thread?
I'm guessing the problem has to do with how WinForms works inside the STA model for threading. Basically, the DataTable you're accessing is located somewhere, and that is probably inside the form we see above. So, when you update the DataTable from another thread, which thread gets the events needed for binding? Likely the thread you update it from, and the form's thread is not aware of the changes being made. So, you simply need to invoke any calls to DataTable onto the form itself, so it receives the events properly:
this.Invoke(() => {
// any calls involving DataTable
});
It seems backwards, but keep in mind in an "enterprise" situation, you'd probably be accessing that dataset by multiple adapters. So, your update thread would have an adapter to itself, and your GUI would have its own also. The other solution would be to use a BindingList, which I believe has thread compatibility for this type of situation, but don't quote me on that.
For extra credit, this could also explain your problem before with crashing. By accessing the DataGridView from the background thread, you had cross-thread operations going on.
I wouldn't call:
gridCurrentResults.Refresh(); // The problematic DataGridView refresh()
gridAllResults.Refresh();
These will take progressively longer and longer as the data set gets larger and larger.
I've written an application that uses a DataGridView to display mp3 file information. I set the DataSource of the DataGridView to a DataTable:
this.dataGridView.DataSource = this.dataTable;
and then simply add the new information to the DataTable:
this.dataTable.Rows.Add(row);
This automatically updates the DataGridView.

Categories

Resources