I have a performance issue with the WPF DataGrid (.net 4.0)
first, some details:
I have a datagrid with an Observable collection as ItemsSource.
this observableCollection itself contains collections of objects, each collection hence being a row, each object being a cell ("logical" cell of course, not actual dataGridCell)
the reason why I do this is because I only know at runtime how many columns I will have in my dataGrid.
then I bind each DataGridCell's value to the value of the object in the "logical" table (= the collection of collections)
now the trouble I have is that I also have to be able to change whatever cell's Properties (like Background, Foreground, FontFamily, etc...) at any time while the app is running.
The solution I came up with is one involving setting the columns' cellStyles with bindings that bind to the "logical" cells' properties
here Is a sample code (no Xaml in my app):
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Width = 1200;
Height = 780;
Top = 60;
Left = 200;
DataGrid dg = new DataGrid();
Content = dg;
ObservableCollection<Row> Source = new ObservableCollection<Row>();
dg.ItemsSource = Source;
dg.SelectionMode = DataGridSelectionMode.Extended;
dg.IsSynchronizedWithCurrentItem = true;
dg.CanUserSortColumns = false;
dg.CanUserReorderColumns = true;
dg.CanUserResizeColumns = true;
dg.CanUserResizeRows = true;
dg.CanUserAddRows = false;
dg.CanUserDeleteRows = false;
dg.AutoGenerateColumns = false;
dg.EnableColumnVirtualization = true;
dg.EnableRowVirtualization = false; // unuseful in my case : I alawys have less lines than the DG can contain
dg.VerticalScrollBarVisibility = ScrollBarVisibility.Visible;
dg.GridLinesVisibility = DataGridGridLinesVisibility.None;
dg.HorizontalGridLinesBrush = Brushes.LightGray;
dg.MinRowHeight = 20;
dg.RowHeaderWidth = 20;
for (int i = 0; i < 100; i++)
{
DataGridTextColumn column = new DataGridTextColumn();
column.Binding = new Binding(String.Format(CultureInfo.InvariantCulture, "[{0}].Text", i));
Style style = new Style(typeof(DataGridCell));
style.Setters.Add(new Setter(DataGridCell.BackgroundProperty, new Binding(String.Format(CultureInfo.InvariantCulture, "[{0}].Background", i))));
style.Setters.Add(new Setter(DataGridCell.ForegroundProperty, new Binding(String.Format(CultureInfo.InvariantCulture, "[{0}].Foreground", i))));
column.CellStyle = style;
column.Header = "Column " + i;
dg.Columns.Add(column);
}
for (int i = 0; i < 35; i++)
{
Row dgRow = new Row();
Source.Add(dgRow);
for (int j = 0; j < 100; j++)
dgRow.Add(new TextBox() { Text = "cell " + i + "/" + j, Background = Brushes.AliceBlue, Foreground = Brushes.BlueViolet });
}
}
}
public class Row : ObservableCollection<TextBox>
{
}
my problem is: with the VolumnVirtualisation On (I don't need row Virtualization in my case), the grid takes about 2sec to load, and then 1sec each time I move the horizontal scrollbar by a big leap (clic in the scrollBar bg, not the arrow)
this is too much for my purpose
so my question is: am I doing something wrong and if yes, what? what better way to do this do I have?
thanks for reading
If ColumnVirtualization make so problems, why do you need it?
You can do a several improvements, but they can't solve the problem completely.
Change TextBoxes for light-weight objects:
public class TextItem
{
public string Text { get; set; }
public Brush Background { get; set; }
public Brush Foreground { get; set; }
}
public class Row : ObservableCollection<TextItem>
{
}
Enable VirtualizingStackPanel: dg.SetValue(VirtualizingStackPanel.IsVirtualizingProperty, true);
Replace styles with templates:
for (int i = 0; i < 100; i++)
{
DataGridTemplateColumn column = new DataGridTemplateColumn();
column.CellTemplate = (DataTemplate)XamlReader.Parse(
"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>" +
"<TextBlock DataContext='{Binding [" + i + "]}' Text='{Binding Text}' Background='{Binding Background}' Foreground='{Binding Foreground}'/>" +
"</DataTemplate>");
column.Header = "Column " + i;
dg.Columns.Add(column);
}
After a lot of time put into this, I came to the conclusion that I've reached the limit.
Here are a few thoughts for those that are dealing with the same issue:
There is no easy way to manage a single cell's visual properties in WPF as of .net 4.0: MS did not plan anything to make this easy so basically you are stuck with 2 possibilities to do this:
get the actual dataGridCell using some kind of helper function and then change its properties directly. This is easily done but can lead to big trouble if virtualization is turned on.
bind each cell's visual properties to dependency properties from your VM inside the dataGridCell's Style. you can use either the DataGrid.CellStyle or the Column.CellStyle to do so, depending on you constraints. This slows the dataGrid quite a bit though, and is quite a hassle to manage.
if like me you have no choice but to use the second option (because I need virtualization), here are a few things to consider:
you are not stuck with C#. There is actually a way to do your CellStyle in Xaml. See Martino's post on this issue. As far as I'm concerned, this works pretty well. I tweaked it a bit so as not to have to use the hack though: I define my style in Xaml and apply it to the Column.CellStyle. Then when I create a column in my code behind, I simply create a new Style inheriting this one, and I add the Tag setter with a binding set to: "[column's Index].Self". This breaks the MVVM model, but I'm not using it anyway and It's easier to maintain like this.
obviously, the more properties you have to bind, the more time it will take for your dataGrid to load, so stick to the minimum (using light-weight objects does make a small difference, as stated by Vorrtex).
while using templates or styles makes absolutely no difference regarding performance, if you are using dataGridTemplateColumns, you'd want to set you bindings up directly in the template instead of adding a style on top of the template, obviously (this does make a small difference with a huge number of data)
if anybody has anything to add to this, please do so! I'm still looking for any idea that can improve things up and would be glad for whatever crazy idea you have on the subject. Even in 3 months...
Related
So I'm creating a simple address book project which will fetch contacts from sql server and display these in a datagridview. When the program first loads, it automatically gets the contacts from the database and populates the grid. I have a method that styles the grid after population - it simply sets some grid properties which all seem to have an affect except the default cell style back color which at first does nothing.
However, when I clear the grid through a custom method and then repopulate the grid through calling the same method as I do when the program first loads, the back color of the grid sets correctly which seems absolutely bizarre. Here's my code:
private void BindGridWithContacts() //Populate grid with list of contact objects - ordered by address id
{
bindingSource.DataSource = typeof(Contacts);
List<Contacts> sortedList = contacts.OrderBy(x => x.AddressId).ToList();
contacts = new BindingList<Contacts>(sortedList); //Orders the list of contacts by ID
foreach (var contact in contacts) //Add each contact object to binding source
{
bindingSource.Add(contact);
}
grdContacts.DataSource = bindingSource; //data grids datasource = binding source
grdContacts.StyleGrid();
}
I've read elsewhere on stack overflow that it can be tricky to style datagrids that are bound to a datasource so at first I just accepted it wouldn't be possible. But then once I called my clear grid method:
private void ClearGrid()
{
grdContacts.DataSource = null;
bindingSource.DataSource = null;
bindingSource.Clear();
}
And then call the BindGridWithContacts() method again, the grid styles with my desired colours. Here is my StyleGrid() method:
public void StyleGrid()
{
//Colour rows - currently not working
for (int i = 0; i < this.RowCount; i++)
{
this.Rows[i].DefaultCellStyle.BackColor = i % 2 != 0 ? Color.Lavender : Color.AliceBlue;
}
//Set Column Widths
this.Columns["AddressId"].Width = 70;
this.Columns["EmailAddress"].Width = 150;
this.Columns["Hobby"].Width = 148;
this.Columns["Forename"].Width = 115;
this.Columns["Surname"].Width = 115;
this.ReadOnly = true;
this.RowHeadersVisible = false;
this.RowHeadersDefaultCellStyle.BackColor = Color.Lavender;
}
Note: the grid will style properly every time the style grid method is called except the first time.
Any suggestions?? Thanks
So I have this idea of adding several buttons to a grid, then arranging them, and the number of buttons added is related to a number I enter. All works fine, until I try to make it work under MVVM.
Code that works in the MainWindow.xaml.cs:
private void Button_Click(object sender, RoutedEventArgs e)
{
int i = 0, j = 0;
Brush[] bs = { Brushes.BurlyWood, Brushes.Honeydew };
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
Button btn = new Button();
btn.Height = 50;
btn.Width=50;
btn.SetValue(Grid.RowProperty, i);
btn.SetValue(Grid.ColumnProperty, j);
if ((i + j) % 2 == 0)
{
btn.Background=bs[0];
}
else btn.Background = bs[1];
GameGrid.Children.Add(btn);
}
}
}
So basically, I press a button and it adds 3x3 buttons, colored nicely and spread appropriately. But then I get to the MVVM:
private void ButtonClickCommand()
{
RowCount = GridNumber;
ColumnCount=GridNumber;
int i = 0, j = 0;
Brush[] bs = { Brushes.BurlyWood, Brushes.Honeydew };
for (i = 0; i < GridNumber; i++)
{
for (j = 0; j < GridNumber; j++)
{
Button btn = new Button();
btn.Height = 50;
btn.Width = 50;
// btn.Command = StartCommand; Ill add that later
btn.SetValue(Grid.RowProperty, i);
btn.SetValue(Grid.ColumnProperty, j);
if ((i + j) % 2 == 0)
{
btn.Background = bs[0];
}
else btn.Background = bs[1];
somebuttonlist.Add(btn);
}
}
}
Here I have a List of buttons, which should accept the new created buttons, then transfer them to the grid. Code of the button list:
private List<Button> _bslist = new List<Button>();
public List<Button> somebuttonlist
{
get
{
return _bslist;
}
set
{
_bslist = value;
NotifyPropertyChanged();
}
}
And xaml code:
<ItemsControl ItemsSource="{Binding somebuttonlist}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid Name="GameGrid" Visibility="{Binding GameVis}"
vm:GridHelpers.RowCount="{Binding RowCount}"
vm:GridHelpers.ColumnCount="{Binding ColumnCount}">
</Grid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Don't mind all the bindings, they work properly when I use the non-MVVM format.
So how do I properly transfer the working code into the View Model? Is there a way to access the children accordingly?
It looks like you are binding to a List, which does not notify when new records are inserted or any record is deleted. The NotifyPropertyChanged() that you have only kicks off when the reference to that list is modified, so for example, setting it to a new List() or a different List. Try changing it to an ObservableCollection instead, which sends an event for when the number of records in the collection get modified.
This is not the right way of doing things with MVVM. What you are trying to do now is to pull visual code into your ViewModel. Ideally, ViewModels should leave in a separate project without any references to visual stuff. This way, referencing Button will result in broken code unless you add a reference for System.Windows.Controls and this reminds you that you do things the wrong way.
What you are trying to do is doable in pure MVVM way, but will result in much more code, which also be convoluted to you if you haven't got a grasp on MVVM yet. Also note, that in real life, it is acceptable to have some code behind for complicated scenarios. For this you just create a custom control to encapsulate all code behind logic.
The nice solution in your case can be a custom control, that encapsulates Grid and logic to populate it with buttons. You also add dependency properties for Rows and Columns, so you can bind to them in XAML, and define a command which is invoked when button is clicked. This may be a good example: How to create bindable commands in Custom control?
Using Mono, MonoDevelop, C# and GTK# on Linux.
I know this has been asked before, but no matter what I try, none of the examples or answers seem to work for me. I still can't get the changes in my ListStore to be reflected in my TreeView.
I start of like so....
ListStore ls_nvDeviceFileDescription = new ListStore (typeof(string), typeof(string), typeof(string));
ListStore ls_dev_sensor_codes = new ListStore (typeof(string));
ListStore ls_dev_sensor_value_types = new ListStore (typeof(string));
The first ListStore is for the TreeView, the second and third is for my CellRendererCombo's on Column 1 and 3 of my TreeView.
I add values to my CellRendererCombo's for the user to pick values from....
string[] _codes = csql.CSQL_GetDeviceSensorCodes ();
string[] _types = csql.CSQL_GetDeviceSensorValueTypes ();
// Do codes.
if (_codes != null) {
for (int i = 0; i < _codes.Length; i++) {
ls_dev_sensor_codes.AppendValues (_codes [i]);
}
}
//Do Types
if (_types != null) {
for (int i = 0; i < _types.Length; i++) {
ls_dev_sensor_value_types.AppendValues (_types [i]);
}
}
I then bind my ListStore to they're appropriate two CellRenderers....
dev_sensor_codes.Model = ls_dev_sensor_codes;
dev_sensor_codes.TextColumn = 0;
dev_sensor_value_types.Model = ls_dev_sensor_value_types;
dev_sensor_value_types.TextColumn = 0;
And of course pack them onto they're respective Columns....
col_sensor_code.PackStart (dev_sensor_codes, true);
col_sensor_descr.PackStart (dev_sensor_descr, true);
col_sensor_value_type.PackStart (dev_sensor_value_types, true);
I then also bind my ListStore for the TreeView and append all my columns....
nvDeviceFileDescr.Model = ls_nvDeviceFileDescription;
nvDeviceFileDescr.AppendColumn (col_sensor_code);
nvDeviceFileDescr.AppendColumn (col_sensor_descr);
nvDeviceFileDescr.AppendColumn (col_sensor_value_type);
I also use the following code append a row to my TreeView (the only change that IS reflected).
ListStore store = (ListStore)nvDeviceFileDescr.Model;
TreeIter iter = store.Append ();
TreePath path = store.GetPath (iter);
nvDeviceFileDescr.SetCursor (path, nvDeviceFileDescr.Columns[0], true);
Now, I'll give two examples of Edited events that I'm sure should work, but doesn't....
Example 1:
dev_sensor_codes.Edited += delegate(object o, EditedArgs args)
{
ListStore store = (ListStore)nvDeviceFileDescr.Model;
TreeSelection sel = nvDeviceFileDescr.Selection;
TreeIter iter;
if (!sel.GetSelected (out iter)) {
return;
}
store.SetValue(iter, 1, args.NewText);
};
Example 2:
dev_sensor_value_types.Edited += delegate(object o, EditedArgs args)
{
ListStore store = (ListStore)nvDeviceFileDescr.Model;
TreeIter iter;
store.GetIterFromString(out iter, args.Path);
store.SetValue (iter, 2, args.NewText);
};
In both examples I know I'm using a good iter, because gtk has good habit of letting you know if you have a stale iter (I think?), and the TreePath, must be good as it's passed through the Edited args.
I can verify that I get all the right iters, paths and values by printing to stdout, but I just can't get the changes in the ListStore for the Treeview reflected in the TreeView.
The only change that does show, is when I programatically AppendValues, or Append. It will add a new line in the TreeView, but no values ever show up.
Many, many thanks to anyone for a hand.
==========================================
SOLUTION
So, as is SO often the case, we answer our own questions because we neglect to read our own code properly right?
I've left out a crucial step in the whole thing above and that is binding the CellRenderers to the actual columns. It 'looked' like I did, but I didn't, so here goes....
I packed the CellRenderers in the columns with the following....
Column.PackStart (CellRenderer, Bool);
//Or in my case
col_sensor_code.PackStart (dev_sensor_codes, true);
This is fine, but I also need to bind the CellRenderer to THAT column (or any other column actually) SO......
Column.AddAttribute (CellRenderer, Attribute, Column Index);
// In my case that would be for instance....
col_sensor_code.AddAttribute (dev_sensor_codes, "text", 0);
So, many thanks for time and hope this helps some other blind fool ;)
I have a TableLayoutPanel that has a dynamic amount of columns and rows determined by the user. I want the buttons inside to be square and the same size, but whenever I use a loop to set the column/rows styles, they never turn out to be the size I want them to be.
How can I get the column/row styles to set the appropriate widths and height os the container elements?
Here is the loop method of the code that handles setting the width size of the table (I use a similar method for rows)
void FormatTableWidth(ref TableLayoutPanel container)
{
TableLayoutColumnStyleCollection columnStyles = container.ColumnStyles;
foreach (ColumnStyle style in columnStyles)
{
style.SizeType = SizeType.Absolute;
style.Width = 60;
}
}
You can do it like....
public void AddButtontControls()
{
tblPanel.SuspendLayout();
tblPanel.Controls.Clear();
tblPanel.GrowStyle = TableLayoutPanelGrowStyle.FixedSize;//.AddColumns;
tblPanel.ColumnStyles.Clear();
for (int i = 0; i < tblPanel.ColumnCount; i++)
{
ColumnStyle cs = new ColumnStyle(SizeType.Percent, 100 / tblPanel.ColumnCount);
tblPanel.ColumnStyles.Add(cs);
//Add Button
Button a = new Button();
a.Text = "Button " + i + 1;
tblPanel.Controls.Add(a);
}
tblPanel.ResumeLayout();
}
Sorry to tell you, but you don't use the right control.
You definitely must use FlowLayoutPanel control, and you can add as many controls you want in it, you can tell which direction will fill the control, if it wrap content or not, and many others.
And the most important - It Will Not Flickering like TableLayoutPanel does :)
I'm writing a simple tic tac toe game for school. The assignment is in C++, but the teacher has given me permission to use C# and WPF as a challenge. I've gotten all the game logic finished and the form mostly complete, but I've run into a wall. I'm currently using a Label to indicate who's turn it is, and I want to change it when a player makes a valid move. According to Applications = Code + Markup, I should be able to use the FindName method of the Window class. However, it keeps returning null. Here's the code:
public TicTacToeGame()
{
Title = "TicTacToe";
SizeToContent = SizeToContent.WidthAndHeight;
ResizeMode = ResizeMode.NoResize;
UniformGrid playingField = new UniformGrid();
playingField.Width = 300;
playingField.Height = 300;
playingField.Margin = new Thickness(20);
Label statusDisplay = new Label();
statusDisplay.Content = "X goes first";
statusDisplay.FontSize = 24;
statusDisplay.Name = "StatusDisplay"; // This is the name of the control
statusDisplay.HorizontalAlignment = HorizontalAlignment.Center;
statusDisplay.Margin = new Thickness(20);
StackPanel layout = new StackPanel();
layout.Children.Add(playingField);
layout.Children.Add(statusDisplay);
Content = layout;
for (int i = 0; i < 9; i++)
{
Button currentButton = new Button();
currentButton.Name = "Space" + i.ToString();
currentButton.FontSize = 32;
currentButton.Click += OnPlayLocationClick;
playingField.Children.Add(currentButton);
}
game = new TicTacToe.GameCore();
}
void OnPlayLocationClick(object sender, RoutedEventArgs args)
{
Button clickedButton = args.Source as Button;
int iButtonNumber = Int32.Parse(clickedButton.Name.Substring(5,1));
int iXPosition = iButtonNumber % 3,
iYPosition = iButtonNumber / 3;
if (game.MoveIsValid(iXPosition, iYPosition) &&
game.Status() == TicTacToe.GameCore.GameStatus.StillGoing)
{
clickedButton.Content =
game.getCurrentPlayer() == TicTacToe.GameCore.Player.X ? "X" : "O";
game.MakeMoveAndChangeTurns(iXPosition, iYPosition);
// And this is where I'm getting it so I can use it.
Label statusDisplay = FindName("StatusDisplay") as Label;
statusDisplay.Content = "It is " +
(game.getCurrentPlayer() == TicTacToe.GameCore.Player.X ? "X" : "O") +
"'s turn";
}
}
What's going on here? I'm using the same name in both places, but FindName can't find it. I've tried using Snoop to see the hierarchy, but the form doesn't show up in the list of applications to choose from. I searched on StackOverflow and found I should be able to use VisualTreeHelper class, but I don't understand how to use it.
Any ideas?
FindName operates on the XAML namescope of the calling control. In your case, since the control is created entirely within code, that XAML namescope is empty -- and that's why FindName fails. See this page:
Any additions to the element tree after initial loading and processing must call the appropriate implementation of RegisterName for the class that defines the XAML namescope. Otherwise, the added object cannot be referenced by name through methods such as FindName. Merely setting a Name property (or x:Name Attribute) does not register that name into any XAML namescope.
The easiest way to fix your problem is to store a reference to your StatusDisplay label in the class as a private member. Or, if you want to learn how to use the VisualTreeHelper class, there's a code snippet at the bottom of this page that walks the visual tree to find the matching element.
(Edited: Of course, it's less work to call RegisterName than to use the VisualTreeHelper, if you don't want to store a reference to the label.)
I'd recommend reading the first link in its entirety if you plan on using WPF/Silverlight in any depth. Useful information to have.
You have to create a new NameScope for your window:
NameScope.SetNameScope(this, new NameScope());
Then you register name of your label with the window:
RegisterName(statusDisplay.Name, statusDisplay);
So this seems to be all you need to do to make FindName() work.