WPF Datagrid Column Width Issue - wpf

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.

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

Binding a DataGrid when already having a DataContext declared in a Window----WPF

I am running into an issue that I haven't been able to resolve and although I've looked at a few similar posts, I haven't found anything that explains my situation.
Basically I have a WPF Window:
<Window x:Class="NewGame">
<DataContext="{Binding RelativeSource={RelativeSource Self}}"/>
In the class I implement INotifyPropertyChanged to utilize bindings for some properties I have set up to update dynamically using XAML. For instance, I have a DB that has Primary, Secondary and Trim Colors(Hex codes) listed for teams, and the properties automatically will update based on changing the team. So I have the BorderBrush, Foreground and Backgrounds on various things auto-updating in XAML using:
<Foreground="{Binding Path=MyPrimColor}">
<Background={Binding Path=MySecColor}">
<BorderBrush={Binding Path=MyTrimColor}">
, etc...each could be any of the properties, it doesn't matter, those are all working fine.
Now, I have a DataGrid which I need to bind to a DataTable to display the players on the team, and that is where I have run into the issue. It tells me the "Items Collection must be empty before using Itemsource" and throws an exception. This was never an issue until I started using the databindings in XAML, when I had things set in code behind, everything worked fine, but I also know this isn't the way things are supposed to be done, which is why I want to have it working with the XAML data-bindings.
I created MyDT a property as DataTable, and when I try to bind
<DataGrid DataContext="{Binding Path=MyDT}">, it causes the Foreground and Background binding paths to try to bind to the Data.DataTable object as well which obviously throws an error.
I have seen some say I need to use <DataGrid.DataContext> inside the <datagrid> but I haven't gotten that to work either. I understand where the problem is coming from---I already have the bindings set at a higher level, but I just don't know how to fix it in XAML by only binding the DataGrid to the DT property while leaving the others to bind to the class level.
Here is the full code section in XAML:
<DataGrid x:Name="TeamRosterDT"
Height="400"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Foreground="{Binding Path=MyTrimColor}"
RowBackground="{Binding Path=MySecColor}"
AlternatingRowBackground="{Binding Path=MyPrimColor}"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserResizeColumns="False"
ColumnWidth="Auto"
HorizontalScrollBarVisibility="Visible"
ItemsSource="{Binding}"
Opacity="0.8"
VerticalScrollBarVisibility="Visible"
Visibility="Hidden"
DataContext="{Binding Path=MyDT}">
<DataGridColumnHeader Style="{StaticResource DataGridHeaderStyle}" />
<DataGridCell HorizontalContentAlignment="Center" />
</DataGrid>
no need to change DataContext (DataContext="{Binding Path=MyDT}"), only bind ItemsSource (<DataGrid ItemsSource="{Binding Path=MyDT}"> or <DataGrid ItemsSource="{Binding Path=MyDT.DefaultView}">)
Exception is thrown because of incorrect items declaration (lines with DataGridColumnHeader, DataGridCell). They are added to Items list which is not supported when ItemsSource is set
I got the Exceptions resolved(I believe) by using:
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="{Binding Path=MyPrimColor}" />
<Setter Property="Foreground" Value="{Binding Path=MyTrimColor}" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<DataGrid.ColumnHeaderStyle>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
</DataGrid.CellStyle>
However, the DataTable does not update properly---when the XAML is initialized its an empty DB, which is then filled and filtered according to what team is selected. When a new team is selected, the DT is simply filtered, not refilled. However, since I suppose this technically isn't a "change" to the DT itself, it doesn't fire the OnPropertyChanged event. How can I get it to update properly using XAML triggers, or is than an event I can utilize when the DB refilters?
Is where I would use an ObservableCollection?

Why is my DataGridComboBoxColumn clearing its value when I navigate away from it?

I have a DataGrid with two columns:
DataGridComboBoxColumn
DataGridTextColumn.
I have set up data validation so that if one has a value, the other will be in error until it also has a value. The validation is silly, but it provides some simple criteria with which to do validation so I can illustrate this issue.
When I type something into the text cell, press tab, then click back on the first cell, the first cell shows that it is in an error state (which is correct). The problem is that when I select something from the combo box dropdown and navigate away from that cell (either by pressing tab or by clicking in another cell), the value I selected for the combo box disappears. I have the binding set to update my source whenever the property changes, so it gets set to the value I select as soon as I select it. But, when I navigate away from the cell, the property gets set to null. I do not see this behavior if the cell is not in an error state.
Can anyone help please? Here is the XAML for my DataGrid:
<DataGrid Grid.Row="2"
Name="GrdData"
ItemsSource="{Binding Path=Dvm.Data}"
SelectedItem="{Binding Path=Dvm.SelectedData, Mode=TwoWay}"
CanUserAddRows="True"
CanUserDeleteRows="False"
AutoGenerateColumns="False"
Margin="5"
SelectionMode="Single"
IsEnabled="{Binding Path=IsGridEnabled}">
<DataGrid.Columns>
<DataGridComboBoxColumn Header="Column 1"
SelectedItemBinding="{Binding Path=Col1, Mode=TwoWay, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"
Width="*"
DisplayMemberPath="Description">
<DataGridComboBoxColumn.ElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding Path=DropDownValues, Mode=OneWay}" />
<Setter Property="IsSynchronizedWithCurrentItem" Value="False"/>
</Style>
</DataGridComboBoxColumn.ElementStyle>
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding Path=DropDownValues, Mode=OneWay}"/>
<Setter Property="IsDropDownOpen" Value="True" />
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
<DataGridTextColumn Header="Column 2"
Binding="{Binding Path=Col2, Mode=TwoWay, ValidatesOnDataErrors=True}"
Width="*"/>
</DataGrid.Columns>
</DataGrid>
I can't imagine what I'm doing wrong. I saw this other link that seems to describe the same problem I am having, but the solution that worked for them doesn't seem to work for me; I added the SelectedValueBinding and SelectedValuePath, but the behavior did not change.
Remove Mode=TwoWay from the bindings.
The problem is caused by a bug in the clipboard and automation support. That works by setting a special property on the cell to ClipboardContentBinding and then reading the value. If that binding is two-way, it winds up sometimes pushing an old value from the special property back to the view model, and validation errors seem to trigger this behavior. DataGridBoundColumns and DataGridComboBoxColumns will supply Binding or SelectedItemBinding if ClipboardContentBinding is null, so you’ll get this bug if you set either of those to a TwoWay binding.
If you don’t set Mode, it will be Default and use the default from the property, which is TwoWay for TextBox.Text and ComboBox.SelectedItem but OneWay for the special clipboard property.

Datagrid scroll bar stops working

When I run the app, the vertical scroll bar works as expected. However, when I add a new line/row, the bar (control that should go up and down on the slider) doesn't slide. With the mouse wheel I can scroll up and down the list of rows, and I can click on the up and down arrows. So the scroll bar works, but not as expected. The control should slide up and down, like it does at first, but after adding that new line, it does not.
I hope that is clear enough, I've searched many issues to find this peculiar behavior, but was unsuccessful. Here is the XAML, in part, as it is now:
<DataGrid x:Name="inventoryDataGrid" AutoGenerateColumns="False"
SelectedValuePath="Id"
EnableRowVirtualization="True"
EnableColumnVirtualization="True"
Style="{DynamicResource DataGridDemoStyle}"
CanUserSortColumns="True"
VerticalAlignment="Top"
ItemsSource="{Binding Source={StaticResource claimInventoryViewSource}}"
RowEditEnding="dgInv_RowEditEnding"
CellEditEnding="dgInv_CellEditEnding"
SelectionChanged="dgInv_SelectionChanged"
IsSynchronizedWithCurrentItem="True" CanUserAddRows="False"
RowHeaderWidth="0"
Sorting="DataGrid_Standard_Sorting" MouseDoubleClick="inventoryDataGrid_DoubleClick"
CanUserDeleteRows="True"
SelectionMode="Single"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Visible"
Width="999.5"
CommandManager.PreviewCanExecute="Grid_PreviewCanExecute" Grid.Column="0"
Grid.Row="1"
Margin="0,3,0,0" RowDetailsVisibilityMode="VisibleWhenSelected" Height="227"
LostFocus="inventoryDataGrid_LostFocus" Background="#FFFCF2E7"
AlternatingRowBackground="#FFF2F2D6" RowBackground="#FF6FC4BF"
GotFocus="inventoryDataGrid_GotFocus">
<DataGrid.Resources>
<Style x:Key="DataGridCellStyle" TargetType="{x:Type DataGridCell}" >
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Foreground" Value="White"/>
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
Thanks!
I was able to solve this issue. The problem was that there was code I implemented a long time ago, in an EndEdit routine (found here: EndEdit equivalent in WPF), which somehow caused this erratic behavior in my datagrid scrollbar.
Once I removed this code, my scrollbar worked without problems. Then of course I had to research a way to save the data in text boxes without the use of EndEdit, but that is way off topic for this Question.

Resources