Performance issues with CellTemplate of GridViewColumn - wpf

I am using a ListView with it's View set to GridView to display items in dynamically generated columns. The ListView's virtualization is enabled. For this example I want to display up to 10 rows with 50 columns (but the count of columns is dynamic).
The first thing I do is to create the columns in code behind in a loop.
"Items" is just an ObservableCollection of strings in my view model for one row.
int index = 0;
foreach(Header header in Headers)
{
GridViewColumn column = new GridViewColumn();
column.Header = // the view model of the header;
column.HeaderTemplate = Resources["HeaderTemplate"] as DataTemplate;
column.DisplayMemberBinding = new Binding("Items" + "[" + index + "]");
m_MyGridView.Columns.Add(column);
index++;
}
Now, when I change the ItemsSource of my ListView, the data gets updated quiet fast.
Unfortunately, each string in every cell has to be formatted (font color, foreground etc). So I cannot use DisplayMemberBinding, but CellTemplate to assign a DataTemplate to every cell.
For this purpose I create a DataTemplate in xaml in my control that holds the ListView. This contains just a TextBlock with some bindings to properties of the cell's view model to define the string style of the TextBlock.
Now, Items is not just a collection of string but a collection of CellViewModels that not only contain the value to display but also information of how to display it. This information is then bound inside the DataTemplate.
The CellTemplate is set up in code and assigned to each GridViewColumn's CellTemplate:
foreach(Header header in Headers)
{
GridViewColumn column = new GridViewColumn();
column.Header = // the view model of the header;
column.HeaderTemplate = Resources["HeaderTemplate"] as DataTemplate;
column.CellTemplate = createCellTemplate(index);
m_MyGridView.Columns.Add(column);
index++;
}
DataTemplate getCellTemplate(int index)
{
FrameworkElementFactory elementFactory = new FrameworkElementFactory (typeof (ContentControl));
elementFactory.SetBinding(ContentControl.ContentProperty, new Binding("Items" + "[" + index + "]"));
DataTemplate dt = Resources["ValueTemplate"] as DataTemplate;
elementFactory.SetValue(ContentControl.ContentTemplateProperty, dt);
DataTemplate dataTemplate = new DataTemplate ();
dataTemplate.VisualTree = elementFactory;
return dataTemplate;
}
When I do this, things get really slow when I'm changing the ItemsSource of the ListView. It now takes nearly a second to render the new cells each time I change the ItemsSource to display different data.
So is there a way to speed things up? I thought about having the formatted string already in my CellViewModel by using a Paragraph, but unfortunately DisplayMemberBinding only supports string, so I have to use the CellTemplate.
Edit: so, here is the listview i'm using:
<ListView ItemsSource="{Binding ElementName=Control, Path=ItemViewModels}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel IsItemsHost="True" VirtualizingStackPanel.IsVirtualizing="True" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.View>
<GridView x:Name="m_MyGridView"/>
</ListView.View>
</ListView>
and the DataTemplate for the cells:
<DataTemplate x:Key="ValueTemplate">
<Border Background="{Binding Path=BackgroundColor">
<TextBlock Text="{Binding Path=Value}"
Foreground="{Binding Path=ForegroundColor}"
FontStyle="{Binding Path=FontStyle}"
FontWeight="{Binding Path=FontWeight}"
</TextBlock>
</Border>
</DataTemplate>
The function getCellTemplate is called only once when the headers are created. When the ItemsSource changes, it is not called, because they have been created already.

Related

WPF GridViewColumn.CellTemplate DataTemplate ContentPresenter

I have a ListView which uses DataTemplates. If i use this in a ListView defined the columns over XAML it works how it schould. My DataTemplates are used in my view. But if i want to use the same DataTemplates in a second ListView, where i add new columns to the ListView it does not use my DataTemplate. What should i Do?
The code in XAML for the first ListView looks like this:
<GridViewColumn x:Name="lvSecondColumn" Header="Value" Width="200">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
My Code i use for generating a column in second ListView is:
DataColumn dc = (DataColumn)colum;
GridViewColumn column = new GridViewColumn( );
column.DisplayMemberBinding = new Binding( dc.ColumnName ) );
column.Header = dc.ColumnName;
TestColumns.Columns.Add( column );
TestListView.ItemsSource = dt.DefaultView;
In WPFInspector i see there is no ContentPresenter in my dynamic Generated Column.
Picture from missing ContentPresenter from WPFInspector
How to add the ContentPresenter to my dynamic column???
You can't set both Binding and DataTemplate. According to the docs
https://msdn.microsoft.com/en-us/library/system.windows.controls.gridviewcolumn.displaymemberbinding(v=vs.110).aspx
The following properties are all used to define the content and style
of a column cell, and are listed here in their order of precedence,
from highest to lowest:
- DisplayMemberBinding
- CellTemplate
- CellTemplateSelector
If you use binding then it will generate a textbox with a ".ToString()" of the bound object. If you are aware of the structure of your items in the ListView you can just make DataTemplates with appropriate bindings in it. However when generating columns dynamically this is an issue.
You can dynamically generate a datatemplate for your column and integrate the binding in it:
public DataTemplate CreateColumnTemplate(string property)
{
StringReader stringReader = new StringReader(
#"<DataTemplate
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">
<ContentPresenter Content=""{Binding " + property + #"}""/>
</DataTemplate>");
XmlReader xmlReader = XmlReader.Create(stringReader);
return XamlReader.Load(xmlReader) as DataTemplate;
}
Then you can generate your columns like this:
GridViewColumn column = new GridViewColumn( );
column.CellTemplate = CreateColumnTemplate(dc.ColumnName);
column.Header = dc.ColumnName;
TestColumns.Columns.Add( column );
I didn't run the code there may be little mistakes.

Set Binding for DataTemplate's elements programmatically

I have such a template
<Window.Resources>
<DataTemplate x:Key="MemberCoefDataTemplate">
<StackPanel>
<CheckBox Name="CheckBox"></CheckBox>
<TextBox Name="TextBox"></TextBox>
</StackPanel>
</DataTemplate>
</Window.Resources>
And i use it in a grid
<DataGridTemplateColumn CellTemplate="{StaticResource MemberCoefDataTemplate}" />
I need to dynamically add columns to the grid. As Binding i use indexer property. So i need to set binding dynamically, because i don't know to which index bind to. When i just for test tried this
var column = new DataGridTemplateColumn();
column.CellTemplate = (DataTemplate)Application.Current.MainWindow.Resources["MemberCoefDataTemplate"];
TextBox tb = column.CellTemplate.FindName("TextBox", dg) as TextBox;
I got InvalidOperationException wit description:
this operation is valid only on elements that have this template applied
Usually to get the control during the setup time, you would have to call the method LoadContent();
I would therefore try column.CellTemplate.LoadContent().
You can find a full explanation about your problem (and a solution of course) written by Josh Smith here on his blog.
Indeed this is the point where the DataGridTemplateColumn applies the DataTemplate:
private FrameworkElement LoadTemplateContent(bool isEditing, object dataItem, DataGridCell cell)
{
DataTemplate dataTemplate;
DataTemplateSelector dataTemplateSelector;
this.ChooseCellTemplateAndSelector(isEditing, out dataTemplate, out dataTemplateSelector);
if (dataTemplate != null || dataTemplateSelector != null)
{
ContentPresenter contentPresenter = new ContentPresenter();
BindingOperations.SetBinding(contentPresenter, ContentPresenter.ContentProperty, new Binding());
contentPresenter.ContentTemplate = dataTemplate;
contentPresenter.ContentTemplateSelector = dataTemplateSelector;
return contentPresenter;
}
return null;
}
As you can see a ContentPresenter is used.

Binding dynamically generated textblock WPF MVVM Light

I am using MVVMM Light WPF and I want to do the following: Generate textboxes dynamically and bind them to a property of a class.
I already have the following but it doesn't show up in my view when running the application.
This is my collection:
private ObservableCollection<Border> _controllekes;
public ObservableCollection<Border> Controllekes
{
get { return _controllekes; }
set
{
_controllekes = value;
RaisePropertyChanged("Controllekes");
}
}
This it my xaml:
<ItemsControl ItemsSource="{Binding Path=Controllekes}">
</ItemsControl>
This is a part where I fill the itemsource "Controllekes":
Controllekes = new ObservableCollection<Border>();
Border border = new Border();
border.BorderThickness = new System.Windows.Thickness(5);
border.BorderBrush = Brushes.AliceBlue;
border.Padding = new System.Windows.Thickness(5);
TextBlock tb = new TextBlock();
tb.Background = Brushes.Red;
Binding nameTextBinding = new Binding("Controllekes");
nameTextBinding.Path = new System.Windows.PropertyPath(this.Dossier.Omschrijving);
nameTextBinding.Mode = BindingMode.OneWay;
//nameTextBinding.Source = this.Dossier.Omschrijving;
tb.SetBinding(TextBlock.TextProperty, nameTextBinding);
border.Child = tb;
this.Controllekes.Add(border);
What it does it creates a border with in this border a textblock where the binding should happen. I whish to bind the property this.Dossier.Omschrijving (Dossier is the class). If I just enter a string in the textbox it works.
In runtime the border gets generated but the textblock remains empty. The object Dossier.Omschrijving contains information.
What do I do wrong?
EDIT:
safe put me in the right direction and the answer of ItemsControl with multiple DataTemplates for a viewmodel made me finish the job :)
through all that and use ItemTemplate
<ItemsControl x:Name="ic">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<CheckBox IsChecked="{Binding yourboolProperty}"/>
<TextBlock Background="Red" Text="{Binding yourStringProperty}"></TextBlock>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
and set ic's ItemsSource to List

dispay data matrix with different number of columns in each row

I need to display ,in a WPF app, a 'data matrix' that has no fixed tabular structure i.e.
data row 1: aaa bb ccc
data row 2: 222 $$ ddddd eeee
...
all data is in a list of arrays.
i need to show this data and enable a user to scroll through it.
the strange data structure comes from a textual flat file, and i cannot change this.
i have read a lot, and experimented with a list-view and a grid-view, but so far i am only able to get:
1. dymanic column creation, but this is no good since i have a different schema for each row
2. rendering each row of data as (delimited/formatted) text. but this is no good since all row data is endign up in a single cell (obviously).
3. (havn't done this yet, hoping to avoid) have lots of data templates with triggers, each with a diffrent column count - say from 5 to 40 - to acomodate vast majority of row types.
my question is: how do i bind this data to a list-like / grid-like scrollable view ?
Thanks in advance
If you don't need the ability to select an item, you can use nested ItemsControls
Simply bind the first ItemsControl to your collection of arrays, and set the ItemTemplate to another ItemsControl which binds to the array of values.
Something like this:
<ScrollViewer Height="300" Width="400">
<ItemsControl ItemsSource="{Binding ListOfArrays}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding }">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding }" Width="50" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
One pass to find the maximum number of columns.
Build a simple Row class that has a property cols string[maxColCount].
Build the maxColCount columns for a GridView in code bind and bind the source to cols[x].
The compiler needs a property but you can bind the column to an collection name index
Syntax for building and binding a GridViewColumn
gvc = new GridViewColumn();
gvch = new GridViewColumnHeader();
gvch.Content = fd.FieldDef.DispName;
gvch.HorizontalContentAlignment = System.Windows.HorizontalAlignment.Stretch;
if (fd.FieldDef.Sort)
{
gvch.Click += new RoutedEventHandler(SortClick);
gvch.Tag = fd.FieldDef.Name;
}
// if (fd.ID == 0 || fd.ID == 1) gvc.Width = 60; sID, sParID
if (!fd.AppliedDispGrid) gvc.Width = 0;
gvc.Header = gvch;
gvBinding = new Binding();
gvBinding.Mode = BindingMode.OneWay;
gvBinding.Path = new PropertyPath("DocFields[" + sDocBaseResultDocsFieldsIndex.ToString() + "].DispValueShort");
template = new DataTemplate();
textblock = new FrameworkElementFactory(typeof(TextBlock));
textblock.SetValue(TextBlock.TextProperty, gvBinding);
textblock.SetValue(TextBlock.TextTrimmingProperty, TextTrimming.WordEllipsis);
// <Setter Property="TextTrimming" Value="WordEllipsis" />
template.VisualTree = new FrameworkElementFactory(typeof(Grid));
template.VisualTree.AppendChild(textblock);
gvc.CellTemplate = template;
gvSearchResults.Columns.Add(gvc);

How to get the active Row of the WPF DataGrid inside the TemplateSelector

I have a WPF Datagrid with three columns. The first two columns DataGridComboBox Columns. The third column is the Template column with ContentControl inside. I want to use this Datagrid as Data entry form for an advanced search. The first column will be binded with the list of properties about a document in the databse (like DocumentName, OwnerName, DateCreated etc). The second column will be a list of Operators (<,<=,!=). These operators will be dynamically changed based on the property selected from the combobox in the
cell[0]. This is handled in the SelectionChanged event of the first comboBox.
Inside the content control(In cell[2]),I need to load a DataTempalte(each contains diffetent UI controls like autocompleteBox,DatePicker,etc) defined in the window's Resources based on the property selected in the comboBox in the first colum. I have added a custom template selector for this purpose. But I am unable to access the DataGrid ComboxBox and its content of the first cell inside the DataTempalte selector code.
Please note that I didn't want to bind existing data to the datagrid. The datagrid will be used as data entry form. So I always want to retain the values selected in the previous rows in edit mode only.
Please refer the sample code from my UI and code behind:
public class DocumentSearchProperty
{
public string PropertyName { get; set; }
public string Operator { get; set; }
public string PropertyValue { get; set; }
}
This is the class which I set as a itemsource for my Datasource.
<DataGrid AutoGenerateColumns="False" Width="Auto" Name="documentPropertyGrid"
ItemsSource="{}"
SelectionMode="Single" CanUserAddRows="True" CanUserDeleteRows="True" CanUserResizeColumns="True" CanUserSortColumns="True"
CanUserResizeRows="False" FrozenColumnCount="0" HorizontalContentAlignment="Stretch" HorizontalAlignment="Stretch" RowHeaderWidth="0"
AlternatingRowBackground="Gainsboro" AlternationCount="2" VerticalContentAlignment="Stretch" VerticalAlignment="Top" VerticalScrollBarVisibility="Auto">
<DataGrid.Columns>
<DataGridComboBoxColumn x:Name="columnProperty" ItemsSource="{}" SelectedItemBinding="{Binding Path=Id}"
Header="Property" Width="170"/>
<DataGridComboBoxColumn x:Name="columnOperator" ItemsSource="{}" SelectedItemBinding="{Binding Path=value}" Header="Operator" Width="170" />
<DataGridTemplateColumn x:Name="PropertyValue" Header="Value" Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentControl x:Name="CntControl">
<ContentControl.Content>1</ContentControl.Content>
</ContentControl>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
The below code is used to handle the selection changed event of the first comboBox. Inside this selection changed event I can get the ComboBox in the cell[1]. But I cannot get the ContentControl in cell[2]. Can anybody suggest a better way to achiveve this?
DataGridRow row = this.documentPropertyGrid.ItemContainerGenerator.ContainerFromIndex(this.documentPropertyGrid.SelectedIndex) as DataGridRow;
ComboBox ele = this.documentPropertyGrid.Columns[0].GetCellContent(row) as ComboBox;
ContentControl contentControl = this.documentPropertyGrid.Columns[2].GetCellContent(row) as ContentControl;
I have made following changes for getting the third column of the grid as content presenter:
ContentPresenter contentPresenter = this.documentPropertyGrid.Columns[2].GetCellContent(row) as ContentPresenter;
After the above change I can now assign a datatemplate dynamically to the contentPresenter as follows in the SelectionChanged event of the comboxBox Column(in the first column).
DataTemplate dt = new DataTemplate();
switch (propertyName)
{
case "DocumentName":
dt = this.FindResource("AutoCompleteBoxTemplate") as DataTemplate;
break;
case "DateCreated":
case "DateModified":
case "DateAccessed":
dt = this.FindResource("DatePickerTemplate") as DataTemplate;
break;
default:
dt = this.FindResource("AutoCompleteBoxTemplate") as DataTemplate;
break;
}
contentPresenter.ContentTemplate = dt;
But now in the second column the Operator values which I have set are replaced with the latest operator in all the rows. For e-g If the property selected in the first combo box is TemplateName there will be a list of two operators (Equal,NotEqual). But when the DateCreated is selected in the first comboBox then the operator column in the second row should display (<,<=,>,>=). But since I am assigning these operators by using the name of the comboxColumn(columnOperator), the operators in the first row also updated with operators for DateCreated. How to restore the values in previous rows?

Resources