WPF: how to recreate ItemContainer? - wpf

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

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 datagrid and Item Container Recycling

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>

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.

WPF ListBoxItem Visibility and ScrollBar

I was hoping to collapse certain ListBoxItems based on a property of their data context.
I came up with the following (trimmed for brevity)
<ListBox ItemsSource="{Binding SourceColumns}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsDeleted}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock VerticalAlignment="Center" Margin="5,0" Text="{Binding ColumnName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This "works" in that it does collapse the listboxitems that are marked as "IsDeleted", however the vertical scrollbar does not adjust for the "missing" items. As I'm scrolling, all of a sudden the bar gets bigger and bigger (without moving) until I scroll past the point of the hidden items, and then finally starts to move.
I also tried explicitly setting the height and width to 0 as well in the data trigger, to no avail.
Does anyone know if there's a workaround for this?
Enter CollectinViewSource
One thing you can do is connect your ListBox to your items through a CollectionViewSource.
What you do is create the collectionViewSource in XAML:
<Window.Resources>
<CollectionViewSource x:Key="cvsItems"/>
</Window.Resources>
Connect to it in your CodeBehind or ViewModel
Dim cvsItems as CollectionViewSource
cvsItems = MyWindow.FindResource("cvsItems")
and set it's source property to your collection of items.
cvsItems.Source = MyItemCollection
Then you can do filtering on it. The collectionViewSource maintains all of the items in the collection, but alters the View of those items based on what you tell it.
Filtering
To filter, create a CollectionView using your CollectionViewSource:
Dim MyCollectionView as CollectionView = cvsItems.View
Next write a filtering function:
Private Function FilterDeleted(ByVal item As Object) As Boolean
Dim MyObj = CType(item, MyObjectType)
If MyObj.Deleted = True Then Return False Else Return True End If
End Function
Finally, write something that makes the magic happen:
MyCollectionView .Filter = New Predicate(Of Object)(AddressOf FilterDeleted)
I usually have checkboxes or Radiobuttons in a hideable expander that lets me change my filtering options back and forth. Those are bound to properties each of which runs the filter function which evaluates all the filters and then returns whether the item should appear or not.
Let me know if this works for you.
Edit:
I almost forgot:
<ListBox ItemsSource="{Binding Source={StaticResource cvsItems}}"/>
The answer is to set the VirtualizingStackPanel.IsVirtual="False" in your listbox.
Why don't my listboxitems collapse?

WPF Data Trigger not working - ComboBox selected index is not getting set to 0

I want to set the SelectedIndex of ComboBox to 0 when the SelectedItem it is bound to is null by using DataTrigger. But it is not working. Where am I going wrong?
The xaml is as follows:
<ComboBox SelectedItem="{Binding MyObject.color_master, Mode=TwoWay}"
ItemsSource="{Binding MyEntities.color_master}"
DislayMemberPath="COLOR_DESCRIPTION" >
<ComboBox.Style>
<Style TargetType="ComboBox">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=MyObject.color_master}" Value="{x:Null}">
<Setter Property="SelectedIndex" Value="0" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
Here MyObject.color_master is null, but still the DataTrigger is not working !
My requirement is very simple, when nothing is selected in combobox, I want the first item to be selected.
It's only a guess but when you use both SelectedItem and SelectedIndex you create dependency on WPF implementation: "who wins?" is an implementation thing. Even if it's documented somewhere it would imply that every developer knows the order (which is not good too, because you never sure who will maintain your code).
I think the simplest thing you can do here is use a single binding to ViewModel's SelectedColorIndex property and let the ViewModel calculate the value based on color_master. So the end result will look like this:
<ComboBox SelectedIndex="{Binding MyObjectViewModel.SelectedColorIndex, Mode=TwoWay}"
ItemsSource="{Binding MyEntities.color_master}"
DislayMemberPath="COLOR_DESCRIPTION" >
</ComboBox>
Update: Since you said your view model can't be touched, here is another option. Write your own IValueConverter which will take an MyObject.color_master and convert it to the index:
<ComboBox SelectedIndex="{Binding MyObject.color_master, Mode=TwoWay, Converter={StaticResouce ColorMasterToIndexConverter}}"
ItemsSource="{Binding MyEntities.color_master}"
DislayMemberPath="COLOR_DESCRIPTION" >
</ComboBox>
Where ColorMasterToIndexConverter defined in a reachable resource dictonary (for say, in the same UserControl.Resources collection).

Resources