WPF datagrid and Item Container Recycling - wpf

In WPF I have a dialog window with an embedded datagrid.
The purpose of the dialog is to select files to upload. I've added a column in the datagrid to allow selection. But there is a fairly complicated relationship between the rows -- uploading one file may require the upload a parent file, for example. Because of that, there is a handler that updates the checkboxes (checking/unchecking and setting isEnabled off/on)
I seem to have run into a problem with "Item Container Recycling" when I get many rows in the datagrid. Some of the definition looks like
<DataGrid Name="myGrid"
...
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Standard">
<DataGrid.Resources>
<Style TargetType="DataGridColumnHeader" x:Key="DataGridHeaderStyle" >
</Style>
<Style x:Key="SingleClickEditing" TargetType="{x:Type DataGridCell}">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="DataGridCell_PreviewMouseLeftButtonDown"></EventSetter>
<EventSetter Event="CheckBox.Checked" Handler="OnChecked"/>
<EventSetter Event="CheckBox.Unchecked" Handler="OnChecked"/>
</Style>
...
When I first created the datagrid I was not able to access all rows of the datagrid until I set:
VirtualizingPanel.VirtualizationMode="Standard"
Now on dialog initialization I find that I can access all rows of the grid and set them up OK.
But the odd thing is that later when the user clicks on a checkbox, the checkbox handler method attempts to access the checkbox from other rows to set them. But then again not all of the rows are available and I see strange results.
It is as if the following got turned off for the datagrid.
VirtualizingPanel.VirtualizationMode="Standard"
Programmatically in the cs code for the dialog I don't see that I am able to access a parameter grid.VirtualizingPanel to see what the value is.
I try to get all the rows of the grid as follows:
private List<DataGridRow> GetDataGridRows(DataGrid grid)
{
var itemsSource = grid.ItemsSource as IEnumerable;
List<DataGridRow> rows = new List<DataGridRow>();
if (null == itemsSource) return null;
foreach (var item in itemsSource)
{
DataGridRow row = grid.ItemContainerGenerator.ContainerFromItem(item) as DataGridRow;
if (row!=null) rows.Add(row);
}
return rows;
}
The problem is that sometimes the following returns null:
grid.ItemContainerGenerator.ContainerFromItem(item)
How I can I make sure that I can always get a list of all rows?
==================================
Added:
The XAML column definition for the checkbox column is as follows:
<DataGrid.Columns>
<DataGridCheckBoxColumn ElementStyle="{StaticResource CenteredCheckStyle}" MinWidth="15"
CellStyle="{StaticResource SingleClickEditing}" Visibility="{Binding exists}"
Binding="{Binding Path=toTransfer, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsReadOnly="False"
CanUserSort="False" CanUserResize="false" CanUserReorder="false">
<DataGridCheckBoxColumn.HeaderTemplate>
<DataTemplate>
<CheckBox Checked="CheckBox_Checked" Unchecked="CheckBox_Checked" Loaded="CheckBox_Loaded"
HorizontalContentAlignment="Center" HorizontalAlignment="Center"
IsThreeState="False" Margin="8 0 0 0" Padding="0 5 0 0"/>
</DataTemplate>
</DataGridCheckBoxColumn.HeaderTemplate>
</DataGridCheckBoxColumn>

Related

Multiple Selection in Datagridcomboboxcolumn

I need a DataGridComboBoxColumn which should allow multiple selection. To do this, I created a DataGridComboBoxColumn populated with CheckBoxes. I handle the Checkbox selection through CommandManager.PreviewExecuted event selecting the check boxes based on stored strings separated by ';' My code:
if (((DataGrid)sender).CurrentCell.Column.DisplayIndex == 6)
{
DataGridRow row = (DataGridRow)SupplierProductsGrid.ItemContainerGenerator.ContainerFromIndex(SupplierProductsGrid.SelectedIndex);
DataGridCell RowColumn = SupplierProductsGrid.Columns[6].GetCellContent(row).Parent as DataGridCell;
ComboBox cb = RowColumn.Content as ComboBox;
if (cb != null)
{
Debug.WriteLine("Entered Combobox editing");
ComboBoxFill(cb, true);
}
}
and in CellEditEnding event I create a string based on the checked box items and store that back to table. My code for that is:
DataGridRow row = (DataGridRow)SupplierProductsGrid.ItemContainerGenerator.ContainerFromIndex(SupplierProductsGrid.SelectedIndex);
DataGridCell RowColumn = SupplierProductsGrid.Columns[6].GetCellContent(row).Parent as DataGridCell;
ComboBox cb = RowColumn.Content as ComboBox;
if (cb != null)
{
//Debug.WriteLine("Entered Combobox Consolidation");
ComboBoxFill(cb, false);
return;
}
The code for checking the comboBox CheckBox items from string and collating the checked items back to string is handled by ComboBoxFill method which works fine. No issue there. I am able to save to the table and retrieve the values. However unless the user select the combobox there is no way user knows what has been selected. I have a previous DataGridTextColumn which provides feedback. I would like to have both the DataGridTextColumn and DataGridComboBoxColumn in one column or a better approach to use multiple select DataGridComboBoxColumn
My Partial XAML looks like this. Of interest is MaterialType Text column and MaterialType combo column. I need to have this under one column so it will be more user friendly.
<DataGridTextColumn Binding="{Binding SupplierLeadTime}" Header="Lead Time" Width="1*" />
<DataGridComboBoxColumn x:Name="StorageUnitCombo" Header="Package Unit" SelectedValuePath="StorageUnitId" Width="2.5*"
DisplayMemberPath="Description" SelectedItemBinding="{Binding BaseStorageUnitNavigation}" />
<DataGridTextColumn x:Name="Materialtype" Header="Material Type" Width="140" Binding="{Binding MaterialType}" Visibility="Visible" ElementStyle="{StaticResource WT}" IsReadOnly="True"/>
<DataGridComboBoxColumn x:Name="MTCombo" Header="Material Type Combo" Width="140" ItemsSource="{Binding Source={StaticResource MaterialTypeViewSource}}" />
Any Help will be greatly appreciated.
After more Google Search and more Browsing of codes, most solutions were not to my requirement nor were that applicable to my solution. However I manage to get the gist of many codes and put together a simple solution that works like a charm. The solution is in the ComboBox Text field. By setting the value for this field, I am able to produce the result that I wanted - Displaying multi selected ComboBox. Here is the XAML code:
<DataGridComboBoxColumn x:Name="MTCombo" Header="Material Type Combo" Width="2.5*"
ItemsSource="{Binding Source={StaticResource MaterialTypeViewSource}}" >
<DataGridComboBoxColumn.ElementStyle>
<Style TargetType="{x:Type ComboBox}">
<Setter Property ="Template">
<Setter.Value>
<ControlTemplate>
<TextBlock Text="{Binding MaterialType}" Style="{StaticResource WTR}"
PreviewTextInput="TextBlock_PreviewTextInput" MouseDown="TextBlock_MouseDown" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGridComboBoxColumn.ElementStyle>
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="IsDropDownOpen" Value="True" />
<Setter Property="Text" Value="Select Material Types"/>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="IsEditable" Value="True"/>
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
Now Only thing was to connect the Text Preview Input & Mouse event to open up the Combobox. That is easily achieved by triggering the BeginEdit method. Here is the code for that:
private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e)
{
SupplierProductsGrid.BeginEdit();
}
The images speaks for themselves. Check the figure for Material Type combo, which displays all the selected elements separated by ;.
Hope this helps others looking for such a feature

WPF: how to recreate ItemContainer?

Following my previous question How to change ComboBox item visibility, since the problem is slightly changed i decided to open a new post to solve the it. For those who don't want to read all the comments on the previous post, here is the situation.
I have a DataGrid that is generated at run time. Each column of this datagrid have a combobox inside the header. All those comboboxes have the same Source, that is an observable collection of a class item. Every item show a property that i use in the ItemContainerStyle of the comboboxes, to decide whether each ComBoBoxItem should be Visible or not.
Now, as far as i know, WPF work this way : if a view contain controls like combobox or treeview, then their items (i.e. ComboBoxItem, TreeViewItem...), won't be generated until it's not necessary (for example when the dropdown of a combobox is opened). If i apply an ItemContainerStyle, this will tell to the target how its items should be created. The problem is, that at the moment that this items are generated, every change i need to apply to the style, won't be saved.
Here is my code:
<DataGrid HeadersVisibility="Column" Name="griglia" Grid.Row="2" ItemsSource="{Binding Path=Test}" AutoGenerateColumns="True" IsReadOnly="True" ScrollViewer.CanContentScroll="True" ScrollViewer.VerticalScrollBarVisibility="Visible" ScrollViewer.HorizontalScrollBarVisibility="Visible">
<DataGrid.ColumnHeaderStyle>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="ContentTemplate" >
<Setter.Value>
<DataTemplate DataType="DataGridColumnHeader" >
<ComboBox ItemContainerStyle="{StaticResource SingleSelectionComboBoxItem}" DisplayMemberPath="Oggetto" Width="100" Height="20" ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},Path=DataContext.Selezione, UpdateSourceTrigger=LostFocus}" SelectionChanged="SingleSelectionComboBox_SelectionChanged">
</ComboBox>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.ColumnHeaderStyle>
</DataGrid>
ItemContainerStyle:
<Style x:Key="SingleSelectionComboBoxItem" TargetType="ComboBoxItem" BasedOn="{StaticResource {x:Type ComboBoxItem}}">
<Style.Triggers>
<DataTrigger Binding="{Binding Selezionato}" Value="True">
<!-- Hide it -->
<Setter Property="Visibility" Value="Collapsed" />
<!-- Also prevent user from selecting it via arrows or mousewheel -->
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
SelectionChanged:
private void SingleSelectionComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (var item in e.RemovedItems.OfType<PopolazioneCombo>())
{
item.Selezionato = false;
}
foreach (var item in e.AddedItems.OfType<PopolazioneCombo>())
{
item.Selezionato = true;
}
}
My requirement is that, if in any of the N combobox an item is selected, then, that item cannot be selected by anyone, until he lose the SelectedItem status. For instance let's assume i have 2 combobox and a collection of 4 Item (x,y,a,b) . If x is selected in ComboBox1, then x can't be selected in none of the 2 ComboBox until the SelectedItem of the ComboBox1 change (from x to y for instance). Now i can even accept the fact that the item in the dropdown is just disabled if it makes the things easier, i just need the fact that it cannot be selected again if he is already selected.
The problem is that this solution work for every ComboBox that has its ItemContainerGenerator.Status = NotStarted (this means that the ComboBoxItem are still not created). If i open the dropdown of a combobox, then its ComboBox items will retain their style no matter what i do (cause ItemContainerGenerator.Status = ContainersGenerated), while the combobox that i didn't opened keep track of the changes in the visibility of the items.
I'm Looking for a solution to recreate these items do that the new style with the changes in the visibility will be applied
after a lucky research on internet (i.e. i've found the correct combination of keyword), i found this wonderful method, so here the updated solution:
private void ComboBox_DropDownOpened(object sender, EventArgs e)
{
ComboBox c = sender as ComboBox;
c.Items.Refresh();
}
add this event in the xaml so that it became:
<ComboBox DropDownOpened="ComboBox_DropDownOpened" ItemContainerStyle="{StaticResource SingleSelectionComboBoxItem}" DisplayMemberPath="Oggetto" Width="100" Height="20" ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},Path=DataContext.Selezione, UpdateSourceTrigger=LostFocus}" SelectionChanged="SingleSelectionComboBox_SelectionChanged"/>
and magically the combobox show the correct changes. Now i'm not a big fan of code behind, so if there is a way to do this adding a property to my Collection, or via xaml it would be better

Animate moving a datagridrow

I'm having an issue using animation to show the translation of a row in a grouped datagrid from one group to another. The animation happens, but the row moves behind other rows on it's way to the new position.
The grouping is done based on the content of one of the cells, so when a user changes the content, the row moves to the new group. Here's some XAML that shows the grid:
<DataGrid x:Name="dataGrid" AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<ItemsPresenter />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="First Name" Binding="{Binding FirstName}"/>
<DataGridTextColumn Header="Last Name" Binding="{Binding LastName}"/>
<DataGridTextColumn Header="Email" Binding="{Binding Email}"/>
<DataGridTemplateColumn Header="Country">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Country}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding Country, UpdateSourceTrigger=PropertyChanged}" TextChanged="TextBoxBase_OnTextChanged"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
To show the animation, I'm firing it off in the TextChanged event handler. The code there locates the DataGridRow and a target point in the new group, and calls a method to do the animation:
private void Animate(FrameworkElement element, Point target)
{
if (element != null)
{
Point sourcePoint = element.PointToScreen(new Point(0, 0));
double yOffset = target.Y - sourcePoint.Y;
element.SetValue(Panel.ZIndexProperty, 10); // this doesn't help
TranslateTransform translateTransform = new TranslateTransform();
element.RenderTransform = translateTransform;
Duration duration = new Duration(TimeSpan.FromSeconds(3));
DoubleAnimation anim = new DoubleAnimation(0, yOffset, duration);
translateTransform.BeginAnimation(TranslateTransform.YProperty, anim);
}
}
This works, but a row moving down to another group is shown going behind the other rows; i.e. the moving row is below and hidden by the other rows along the way. Setting ZIndex on the DataGridRow doesn't seem to do anything. Also note that moving a row up works fine -- the moving row is shown over the other rows. I've also tried firing the animation off of events other than TextChanged (which I expect I'll need to do eventually anyway) but I haven't found any that seem to matter.
Thanks.
I've come up with an answer for my question. The hint I needed came from this Silverlight post. In particular, the line "All the list items must be siblings if you want to lay them out in layers." What was going on for me was that because of the grouping, the rows weren't siblings of each other -- they were not direct children of a common parent. Instead, it was the GroupItems containing the rows that were the siblings. So I needed to add to my Animate() method the code:
GroupItem groupItem = TryFindParent<GroupItem>(element);
groupItem.SetValue(Panel.ZIndexProperty, 10);
where element is the DataGridRow to be moved.
I still need to come up with a better trigger for the animation, but at least the row is moving in front of the other rows along the path.

Cancel Edit Mode in WPF DataGrid even when validation fires

I'm new to wpf, I need to use a WPF DataGrid which has its ItemSource set to an ObservableCollection of type Model, where Model implements the IDataErrorInfo class.
The problem I am facing is that if the Model returns a validation string for any property, then the user is unable to exit the edit mode for the cell, I tried rollback & I even tried CancelEdit, but I can't exit the edit mode. I searched on msdn and I found out the its one of DataGrid's property but I need to do the same because of some application requirements.
My Model Class:
public class Model: IDataErrorInfo
{
public int PropertyName{ get; set; }
// other properties & methods removed for clarity
public string this[columnName]
{
get
{
if (PropertyName< 0)
return "Error Message";
else
return string.Empty;
}
}
}
Now if 'PropertyName' is less than 0, the user cannot exit the edit mode of the specific cell.
This link on msdn says in 'Remarks' that
The DataGrid will not exit cell editing mode until the validation
error is resolved.
Is there any workaround to exit the cell edit mode even if the Validation has returned an error message? I can't help with the code architecture because I am stuck with DataGrid as well as the 'Model' class. Any help would be appreciated, Thanks a lot in advance.
Validating WPF DataGrid using IdataErrorInfo,
The DataGrid will not exit cell editing mode until the validation error is resolved.
This is true but can overcome with a work around - using the TextBox in the DataGridTemplateColumn.CellEditingTemplate and DataGridTemplateColumn.CellTemplate.
Define a datatemplate for datagrid
<DataGrid.Resources>
....
<DataTemplate x:Key="EditingValueTemplate">
<TextBox Text="{Binding bindingProp, ValidatesOnDataErrors=True}"
FocusManager.FocusedElement="{Binding RelativeSource= {RelativeSource Self}}"/>
</DataTemplate>
</DataGrid.Resources>
assign this data template to CellTemplate and CellEditingTemplate of the data grid
<DataGrid.Columns>
<DataGridTemplateColumn Header="Value" CellTemplate="{StaticResource EditingValueTemplate}"
CellEditingTemplate="{StaticResource EditingValueTemplate}" />
</DataGrid.Columns>
In case you want to assign tool tip to show error both cases i.e. cell edit mode and mouse hover cell.
You need a style
<Style x:Key="DatagridCellToolTip" TargetType="{x:Type DataGridCell}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
and include this style as well with the above datagrid i.e.
<DataGrid.Columns>
<DataGridTemplateColumn Header="Value" CellTemplate="{StaticResource EditingValueTemplate}"
CellEditingTemplate="{StaticResource EditingValueTemplate}"
CellStyle="{StaticResource DatagridCellToolTip}"/>
</DataGrid.Columns>
I just recognized this by coincident.
When you replace every DataGridTextColumn to TemplateColumn and just make your Binding to the TextBox you can jump between every TextBox and edit multiple rows even if error is not fixed.
<DataGridTemplateColumn Header="Sample" Width="1*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox VerticalAlignment="Stretch" HorizontalAlignment="Stretch" >
<TextBox.Text>
<Binding Path="SampleB" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<vm:CellDataInfoValidationRule ValidationStep="UpdatedValue"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

WPF Datagrid Column Width Issue

I have columns in a DataGrid that are being set by an ObservableCollection that is the type of a simple data object that I created. The first column has a width set to "Auto" and the second column as a width set to "1*".
I am currently using the method in the answer here to autoupdate the width of my column that is set to "Auto" when the ItemsSource changes. This seems to work most of the time:
This looks great, and works all of the time
Although, when the ItemsSource is a little bit larger (lets say about 30-35 records), the "Auto" width (first) column will shrink down only when the DataGrid (including the scroll bar) is clicked:
This will be resized properly if it hasn't been clicked
My XAML looks like this:
<my:DataGrid CanUserSortColumns="false" CanUserResizeRows="false" CanUserResizeColumns="false" CanUserReorderColumns="false" CanUserDeleteRows="false" CanUserAddRows="false" AutoGenerateColumns ="False" SelectionMode="Single" SelectionUnit="Cell" Height="113" HorizontalAlignment="Left" Margin="11,22,0,0" Name="dataGrid" VerticalAlignment="Top" Width="226" Background="#FFE2E2E2" AlternatingRowBackground="#FFA4CFF2" BorderBrush="#FF7C7C7C" HorizontalGridLinesBrush="White" PreviewKeyDown="dataGrid_PreviewKeyDown" CellEditEnding="dataGrid_CellEditEnding" BeginningEdit="dataGrid_BeginningEdit" PreparingCellForEdit="dataGrid_PreparingCellForEdit" SelectedCellsChanged="dataGrid_SelectedCellsChanged" Loaded="dataGrid_Loaded" TargetUpdated="dataGrid_TargetUpdated">
<my:DataGrid.Columns>
<my:DataGridTextColumn Binding="{Binding Path=Name, NotifyOnTargetUpdated=True}" Width="Auto">
<my:DataGridTextColumn.CellStyle>
<Style TargetType="{x:Type my:DataGridCell}">
<Setter Property="KeyboardNavigation.IsTabStop" Value="False"></Setter>
<Setter Property="IsHitTestVisible" Value="False"></Setter>
<Setter Property="Focusable" Value="False"></Setter>
<Setter Property="Background" Value="WhiteSmoke"></Setter>
<Setter Property="BorderBrush" Value="LightGray"></Setter>
</Style>
</my:DataGridTextColumn.CellStyle>
</my:DataGridTextColumn>
<my:DataGridTextColumn Binding="{Binding Path=Value}" Width="1*"></my:DataGridTextColumn>
</my:DataGrid.Columns>
</my:DataGrid>
The code to assure the updating of the column:
private void dataGrid_TargetUpdated(object sender, DataTransferEventArgs e)
{
dataGrid.Columns[0].Width = 0;
dataGrid.UpdateLayout();
dataGrid.Columns[0].Width = new DataGridLength(0, DataGridLengthUnitType.Auto);
dataGrid.UpdateLayout();
}
Is there any reason this may be happening only when the list is longer like this?
DataGrid's TargetUpdated might not get called in a few scenarios. For example, when you have more rows coming in but they are not visible then the datagrid doesn't have to "waste cycles on" re-rendering something that is not visible. The initial TargetUpdated is fine, but you might have to find an additional hook, and do similar thing there, such as hooking into the CollectionChanged of the object that's bound to ItemsSource of your datagrid, your observableCollection has the event CollectionChanged, subscribe and try your logic there.

Resources