I have a DataGrid bound to an ICollectionView in my ViewModel. The DataGrid is inside a UserControl which is used in a few different data scenarios, some of which require certain DataGrid columns while others don't.
I just want to bind the DataGridTemplateColumn's Visibility property to the inner label's Content property so if none of the rows contain a value, it will be hidden. I have a String to Visibility converter set, but can't figure out how to find the inner lable's Content property.
<DataGridTemplateColumn Header="Groups" Width="*" CanUserSort="True" SortMemberPath="Groups" Visibility="{Binding ElementName=lbl, Path=Content, Converter={StaticResource StringToVisibilityConverter}}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Label Name="lbl" Content="{Binding Path=Groups}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Any suggestions?
I read somewhere on Stack Overflow(can't find exact post) that the DataGridColumn's aren't assigned a data context because they aren't a FrameworkElement. To get around this, I had to use code similiar to this:
<DataGridTemplateColumn
Header="Groups"
Width="*"
CanUserSort="True"
SortMemberPath="Groups"
Visibility"{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(FrameworkElement.DataContext).IsGroupsVisible,
Converter={StaticResource booleanToVisiblityConverter}}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Label Name="lbl" Content="{Binding Path=Groups}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Where
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="booleanToVisibilityConverter" />
</UserControl.Resources>
To use RelativeSource.Self as a RelativeSource binding for a DataGridTemplateColumn - you need to add the DataGridContextHelper to your application. This is still required for the WPF 4 DataGrid.
<DataGridTemplateColumn
Header="Groups"
Width="*"
CanUserSort="True"
SortMemberPath="Groups"
Visibility"{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(FrameworkElement.DataContext).IsGroupsVisible,
Converter={StaticResource booleanToVisiblityConverter}}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Label Name="lbl" Content="{Binding Path=Groups}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
This would be better achieved going through the Groups property on the ViewModel; since that is ultimately what the Label is using anyways.
<DataGridTemplateColumn Header="Groups" Width="*" CanUserSort="True" SortMemberPath="Groups" Visibility="{Binding Groups, Converter={StaticResource SomeConverter}}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Label Name="lbl" Content="{Binding Path=Groups}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
One hundred thanks to SliverNinja and this article DataGridContextHelper. Links to source code already not working and was not able download, so i wrote my own Attached Proeprty to make it work for all possible cases (DataContext changed, Attached Property value changed, Column added)
My application use DataGrid with AutoGenerateColumns=False and use DataGridTemplateColumn, so DataContext was set before columns added to grid.
Here is Attached Property class:
public sealed class DataGridColumnDataContextForwardBehavior
{
private DataGrid dataGrid = null;
public DataGridColumnDataContextForwardBehavior(DataGrid dataGrid)
{
this.dataGrid = dataGrid;
dataGrid.Columns.CollectionChanged += DataGridColumns_CollectionChanged;
}
private void DataGridColumns_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
var IsDataContextForwardingEnabled = GetIsDataContextForwardingEnabled(dataGrid);
if (IsDataContextForwardingEnabled && dataGrid.DataContext != null)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (DataGridColumn column in e.NewItems)
{
column.SetValue(FrameworkElement.DataContextProperty, dataGrid.DataContext);
}
}
}
}
static DataGridColumnDataContextForwardBehavior()
{
FrameworkElement.DataContextProperty.AddOwner(typeof(DataGridColumn));
FrameworkElement.DataContextProperty.OverrideMetadata(typeof(DataGrid),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, new PropertyChangedCallback(OnDataContextChanged)));
}
public static readonly DependencyProperty IsDataContextForwardingEnabledProperty =
DependencyProperty.RegisterAttached("IsDataContextForwardingEnabled", typeof(bool), typeof(DataGridColumnDataContextForwardBehavior),
new FrameworkPropertyMetadata(false, OnIsDataContextForwardingEnabledChanged));
public static void OnDataContextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = obj as DataGrid;
if (dataGrid == null) return;
var IsDataContextForwardingEnabled = GetIsDataContextForwardingEnabled(dataGrid);
if (IsDataContextForwardingEnabled)
{
foreach (DataGridColumn col in dataGrid.Columns)
{
col.SetValue(FrameworkElement.DataContextProperty, e.NewValue);
}
}
}
static void OnIsDataContextForwardingEnabledChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var dataGrid = obj as DataGrid;
if (dataGrid == null) return;
new DataGridColumnDataContextForwardBehavior(dataGrid);
if (!(e.NewValue is bool)) return;
if ((bool)e.NewValue && dataGrid.DataContext != null)
OnDataContextChanged(obj, new DependencyPropertyChangedEventArgs(FrameworkElement.DataContextProperty, dataGrid.DataContext, dataGrid.DataContext));
}
public static bool GetIsDataContextForwardingEnabled(DependencyObject dataGrid)
{
return (bool)dataGrid.GetValue(IsDataContextForwardingEnabledProperty);
}
public static void SetIsDataContextForwardingEnabled(DependencyObject dataGrid, bool value)
{
dataGrid.SetValue(IsDataContextForwardingEnabledProperty, value);
}
}
Another non obvious things is how to properly use binding for DataGridTemplateColumn:
<DataGrid bhv:DataGridColumnDataContextForwardBehavior.IsDataContextForwardingEnabled="True">
<DataGrid.Columns>
<DataGridTemplateColumn Visibility="{Binding Path=DataContext.Mode, RelativeSource={RelativeSource Self}, Converter={StaticResource SharedFilesModeToVisibilityConverter}, ConverterParameter={x:Static vmsf:SharedFilesMode.SharedOut}}"/>
It is important to use Path=DataContext.MyProp and RelativeSource Self
Only thing i don't like in current implementation - to handle DataGrid.Columns.CollectionChanged event i create instance of my class and do not keep reference for it. So in theory GC may kill it within the time, not sure how to handle it correctly at present moment, will think on it and update my post later. Any ideas and critique are welcome.
You can't do this. Binding/name resolution doesn't work this way. Why not, instead of a StringToVisibilityConverter create a CollectionToVisibilityConverter which examines the data source (possibly passing in the column/property to examine), looks to see if that column/property is completely empty, and then convert that to a Visibility?
Related
<DataGrid>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=test}"></DataGridTextColumn>
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<DataGrid Template="{DynamicResource TemplateDataGridPrintAndExport}"/>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
<DataGrid/>
I have a datagrid like above. Datgrid's row detail template also contains a datagrid. Inner datagrid is filled when the parent one's columns are clicked. My problem is this : if the row detail template datagrid is fulfilled and user mouse hovers on it while scrolling parent datagrid the scroll is not working. User should hover the mouse to the main datagriid to scroll. However, it is not user friendly. How can I prevent inner datagrid behaving in such a way?
I found the soultion by trying alternatives :
<DataGrid ScrollViewer.CanContentScroll="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=test}"></DataGridTextColumn>
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<DataGrid Template="{DynamicResource TemplateDataGridPrintAndExport}" IsReadOnly="True" ScrollViewer.CanContentScroll="False" IsEnabled="False"/>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
<DataGrid/>
The solution is to give ScrollViewer.CanContentScroll="False" attribute to the outer data grid and IsReadOnly="True" ScrollViewer.CanContentScroll="False" IsEnabled="False" attributes to inner datagrid. Now it is scrolling smoothly and accoording to the parent datagrid.
I would like to propose two alternative solutions, since the chosen one has serious side effects. One of them mentioned by Kaizen - you lose ability to interact with nested DataGrid and its child controls. Second one is the change of appearence of controls in their disabled state.
Change IsReadOnly="True" to IsHitTestVisible="False" in osmanraifgunes' solution. This will fix the appearence side effect, but you still won't be able to interact with inner controls (using mouse). Code:
<DataGrid ScrollViewer.CanContentScroll="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=test}" />
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<DataGrid
IsHitTestVisible="False"
ScrollViewer.CanContentScroll="False"
Template="{DynamicResource TemplateDataGridPrintAndExport}" />
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
Catch the tunneling PreviewMouseWheel event in the control within RowDetailsTemplate and pass it back to parent as a bubbling event. This will effectively make controls within RowDetailsTemplate blind only to mouse scrolling, and allow controls above in visual tree to handle it however they want to.
xaml:
<DataGrid ScrollViewer.CanContentScroll="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=test}" />
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<DataGrid
PreviewMouseWheel="DataGrid_PreviewMouseWheel"
Template="{DynamicResource TemplateDataGridPrintAndExport}" />
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
code behind:
private void DataGrid_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (e.Handled)
{
return;
}
Control control = sender as Control;
if(control == null)
{
return;
}
e.Handled = true;
var wheelArgs = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = MouseWheelEvent,
Source = control
};
var parent = control.Parent as UIElement;
parent?.RaiseEvent(wheelArgs);
}
If you're using .NET 4.5 and above you can use VirtualizingPanel.ScrollUnit="Pixel" on the outer grid, which will allow you to scroll by pixels instead of units (items) as that is causing pretty weird behavior when having big inner DataGrid as it starts jumping around.
Then you can just past scrolling event to the parent using PreviewMouseWheel event on the inner DataGrid since it is being captured by the inner control.
Xaml:
<DataGrid VirtualizingPanel.ScrollUnit="Pixel">
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<DataGrid PreviewMouseWheel="DataGrid_PreviewMouseWheel"/>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
cs:
private void DataGrid_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = true;
var parent = ((Control)sender).Parent as UIElement;
parent?.RaiseEvent(new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = MouseWheelEvent,
Source = sender
});
}
I've used #Bartłomiej Popielarz's 2nd approach to make it work.
In my case somehow control.Parent always returned null. Thats why I've changed the according line to
var parent = VisualTreeHelper.GetParent(control) as UIElement;
Also I've created a attached Property that does the forwarding (better suited for MVVM approaches).
public class FixScrollingBehaviorOn
{
public static readonly DependencyProperty ParentDataGridProperty = DependencyProperty.RegisterAttached("ParentDataGrid", typeof(DataGrid), typeof(FixScrollingBehaviorOn),
new FrameworkPropertyMetadata(default(DataGrid), OnParentDataGridPropertyChanged));
public static bool GetParentDataGrid(DependencyObject obj)
{
return (bool)obj.GetValue(ParentDataGridProperty);
}
public static void SetParentDataGrid(DependencyObject obj, bool value)
{
obj.SetValue(ParentDataGridProperty, value);
}
public static void OnParentDataGridPropertyChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null)
{
throw new ArgumentException("The dependency property can only be attached to a DataGrid", "sender");
}
if (e.NewValue is DataGrid parentGrid)
{
dataGrid.PreviewMouseWheel += HandlePreviewMouseWheel;
parentGrid.SetValue(ScrollViewer.CanContentScrollProperty, false);
}
else
{
dataGrid.PreviewMouseWheel -= HandlePreviewMouseWheel;
if (e.OldValue is DataGrid oldParentGrid)
{
oldParentGrid.SetValue(ScrollViewer.CanContentScrollProperty, ScrollViewer.CanContentScrollProperty.DefaultMetadata.DefaultValue);
}
}
}
private static void HandlePreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (e.Handled)
{
return;
}
var control = sender as DataGrid;
if (control == null)
{
return;
}
e.Handled = true;
var wheelArgs = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = control
};
var parent = VisualTreeHelper.GetParent(control) as UIElement;
parent?.RaiseEvent(wheelArgs);
}
Feel free to use this as follows on your inner DataGrid. Note that The ScrollViewer.CanContentScrollProperty is being set from within the AttachedProperty, which may not be everyone's favourite approach.
<DataGrid>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=test}" />
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<DataGrid
attachedProperties:FixScrollingBehaviorOn.ParentDataGrid="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}"
Template="{DynamicResource TemplateDataGridPrintAndExport}" />
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
Xaml :
<DataGrid ScrollViewer.CanContentScroll="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=test}" />
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<DataGrid
PreviewMouseWheel="DataGrid_PreviewMouseWheel"
Template="{DynamicResource TemplateDataGridPrintAndExport}" />
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
Code behind :
private void DataGrid_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
DataGrid dg = new DataGrid();
if (sender is DataGrid)
{
dg = (DataGrid)sender;
}
dg.IsEnabled = false;
await Task.Delay(200);
dg.IsEnabled = true;
}
I am trying to create a DataGrid in WPF 4.0 using MVVM...
Features required -
Muti - Select rows using a checkbox (single click)
A select all checkbox to check all the checkboxes in the datagrid
Something like this -
It has been 2 days and i am not able to figure out how to solve the problem effectively..
A working example is what I need now ASAP..
I'll highly appreciate if someone has a working solution to share with me...
N please don't tell me to google this thing because none of the things worked out for me...
UPDATE -
I am using AutoGeneration of Columns
I don't want to add "IsSelected" or any of such property in my MODEL..
I am just facing 2 problems -
Firstly, "Select all" Feature i.e. checking all checkboxes on the checkbox click of the one present in the column header...(I am able to select and unselect the datagrid but not able to tick/untick the checkboxes)
Secondly, Multiple selection on mouse click without holding Ctrl Key..
When you're working with MVVM, you have to be aware of what is considered data and what is strictly UI.
Is your SelectedItems going to be part of your data, or only your UI?
If it's part of your data, you really should have an IsSelected property on your data model, even if that means extending the data class to include an IsSelected property, or creating a wrapper class that only contains bool IsSelected and object MyDataItem. The first option is probably preferred, since you could keep AutoGenerateColumns="True", and it makes the column bindings simpler.
Then you would just bind your DataGridRow.SelectedItem to the IsSelected property of the data item:
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
But if your SelectedItems is only for the UI, or if you are breaking the MVVM pattern for some reason in this instance, than you can create the unbound CheckBox and use some code behind to ensure the CheckBox is correctly synchronized to the SelectedItem.
I did a quick sample app, and here is what my code looked like:
First off, I just added the unbound CheckBox column to the column list using a DataGridTemplateColumn. This will get added before the AutoGenerateColumns list of columns.
<DataGrid x:Name="TestDataGrid" ItemsSource="{Binding Test}"
SelectionMode="Extended" CanUserAddRows="False"
PreviewMouseLeftButtonDown="TestDataGrid_PreviewMouseLeftButtonDown_1">
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox x:Name="TestCheckBox"
PreviewMouseLeftButtonDown="CheckBox_PreviewMouseLeftButtonDown" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Second, I added a PreviewMouseDown event to the CheckBox to make it set the IsSelected property of the row.
private void CheckBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var chk = (CheckBox)sender;
var row = VisualTreeHelpers.FindAncestor<DataGridRow>(chk);
var newValue = !chk.IsChecked.GetValueOrDefault();
row.IsSelected = newValue;
chk.IsChecked = newValue;
// Mark event as handled so that the default
// DataGridPreviewMouseDown doesn't handle the event
e.Handled = true;
}
It needs to navigate the VisualTree to find the DataGridRow associated with the clicked CheckBox to select it, and to make life easier I am using some custom VisualTreeHelpers that I have on my blog to find the DataGridRow. You can use the same code, or you can create your own method for searching the VisualTree.
And last of all, if the user clicks on anywhere other than the CheckBox, we want to disable the default DataGrid selection event. This ensures that the IsSelected value will only change when you click on the CheckBox.
There are multiple ways of doing this that will disable the selection at different levels, but to make life simple I just disabled the DataGrid.PreviewMouseLeftButtonDown event if the user didn't click on the CheckBox.
private void TestDataGrid_PreviewMouseLeftButtonDown_1(object sender, MouseButtonEventArgs e)
{
var chk = VisualTreeHelpers.FindAncestor<CheckBox>((DependencyObject)e.OriginalSource, "TestCheckBox");
if (chk == null)
e.Handled = true;
}
I using my custom VisualTreeHelpers again to navigate the visual tree and find out if the CheckBox was clicked on, and cancelling the event if the user clicked on anywhere other than the CheckBox.
As for your 2nd request of adding a CheckBox to SelectAll or UnselectAll items, this would once again be dependent on if your selection is part of the UI or the data.
If it's part of the UI, simply add a CheckBox to the DataGridTemplateColumn.HeaderTemplate, and when it's clicked, loop through the DataGrid.Rows, find the CheckBox in the first column, and check or uncheck it.
If it's part of the data you could still do the same thing (only set the bound value in the DataGrid.Items instead of the CheckBox.IsChecked from the DataGrid.Rows), or you could do as Adolfo Perez suggested, and bind it to a property on the ViewModel.
For an MVVM Solution you could try this:
<StackPanel>
<DataGrid ItemsSource="{Binding Path=TestItems}" AutoGenerateColumns="False" Name="MyDataGrid"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridCheckBoxColumn Binding="{Binding IsSelected}" Width="50" >
<DataGridCheckBoxColumn.HeaderTemplate>
<DataTemplate x:Name="dtAllChkBx">
<CheckBox Name="cbxAll" Content="All" IsChecked="{Binding Path=DataContext.AllSelected,RelativeSource={RelativeSource AncestorType=DataGrid}}"/>
</DataTemplate>
</DataGridCheckBoxColumn.HeaderTemplate>
</DataGridCheckBoxColumn>
<DataGridTemplateColumn Header="Name" Width="SizeToCells" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
In your ViewModel:
private void PopulateTestItems()
{
TestItems = new ObservableCollection<TestItem>();
for (int i = 0; i < 5; i++)
{
TestItem ti = new TestItem();
ti.Name = "TestItem" + i;
ti.IsSelected = true;
TestItems.Add(ti);
}
}
private bool _AllSelected;
public bool AllSelected
{
get { return _AllSelected; }
set
{
_AllSelected = value;
TestItems.ToList().ForEach(x => x.IsSelected = value);
NotifyPropertyChanged(m => m.AllSelected);
}
}
private ObservableCollection<TestItem> _TestItems;
public ObservableCollection<TestItem> TestItems
{
get { return _TestItems; }
set
{
_TestItems = value;
NotifyPropertyChanged(m => m.TestItems);
}
}
And finally the sample Model class:
public class TestItem : ModelBase<TestItem>
{
private string _Name;
public string Name
{
get { return _Name; }
set
{
_Name = value;
NotifyPropertyChanged(m => m.Name);
}
}
private bool _IsSelected;
public bool IsSelected
{
get { return _IsSelected; }
set
{
_IsSelected = value;
NotifyPropertyChanged(m => m.IsSelected);
}
}
}
Most of the code above should be self-explanatory but if you have any question let me know
Your view can be something like
<DataGrid Name="SomeDataGrid" Grid.Row="0" ItemsSource="{Binding Path=SomeCollection}">
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.HeaderTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding
RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},
Path=DataContext.AllItemsAreChecked}" />
</DataTemplate>
</DataGridTemplateColumn.HeaderTemplate>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type local:SomeType}">
<CheckBox Focusable="False" IsChecked="{Binding Path=IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="RandomNumber" Width="160">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type local:SomeType}">
<TextBlock Text="{Binding Path=RandomNumber}" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Date" Width="160">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type local:SomeType}">
<TextBlock Text="{Binding Path=Date}" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Time" Width="50">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type local:SomeType}">
<TextBlock Text="{Binding Time}" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
And in viewmodel
SomeCollection binding property is an observablecollection
sometype contains properties like IsSelected , RandomNumber ,Date , Time
for eg:
class ViewModel
{
public ObservableCollection<SomeType> SomeCollection{get;set;}
}
class SomeType
{
public string Date {get;set;}
public string Time {get;set;}
public string RandomNumber {get;set;}
public bool IsSelected {get;set;}
}
I want to enable the user to edit some data in WPF DataGrid ( from the .net Framework 4.0). The "instruments" column should allow the user to select an available intrument from a static list or to write a free text.
My DataGrid is binded to data using MVVM. I've tried many solutions I've found in internet but none of them work correctly.
Here is my code:
<DataGrid Margin="0,6" ItemsSource="{Binding Path=Orders}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" CanUserResizeRows="True">
<DataGrid.Columns>
<DataGridComboBoxColumn Header="Instrument" MinWidth="140"
ItemsSource="{x:Static ViewModel.Instruments}" SelectedItemBinding="{Binding Path=SelectedInstrument}">
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="IsEditable" Value="True"/>
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
</DataGrid.Columns>
</DataGrid>
The drop-down-list is shown correctly. The field can be edited with any text, but it sets a null to the SelectedInstrument after the drop-down is closed for the free text. It works only for the selected item. I've tried to change to SelectedValueBinding, but it doesn't help.
How to implement this requirements properly? Can someone post here a working sample?
Additional:
Orders is ObservableCollection
Order has Property like string Title, DateTime Ordered, string SelectedInstrument,
Instruments is a string[]
Solutions:
Following suggest as a workaround from bathineni works:
<DataGrid Margin="0,6" ItemsSource="{Binding Path=Orders}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" CanUserResizeRows="True">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Instrument" MinWidth="140">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=SelectedInstrument, Mode=OneWay}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox IsEditable="True" Text="{Binding Path=SelectedInstrument}"
ItemsSource="{x:Static ViewModel.Instruments}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
this is happening because the free text which is enter is of type string and selected item what you have binded to the comboBox is of some complex type....
instead of using DataGridComboBoxColumn use DataGridTemplateColumn and you can bind Text property of the comboBox to some property which will hold the free text value after closing drop down list.
you can get better idea by looking at the following sample.
<DataGrid>
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox IsEditable="True"
Text="{Binding NewItem}"
ItemsSource="{Binding Sourcelist.Files}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Try to use SelectedValue only but along with it use DisplayMemberPath and TextSearch.TextPath.
<ComboBox IsEditable="True" DisplayMemberPath="MyDisplayProperty" SelectedValuePath="MyValueProperty" SelectedValue="{Binding MyViewModelValueProperty}" TextSearch.TextPath="MyDisplayProperty" />
For editable comboboxes we must synchronize what value the combo selects, what value the items display and what value we must search based on user input.
But If you are using a string collection to bind your combobox then you can try following...
Add a new property in your ViewModel called InstrumentsView. This returns a new ListCollectionView.
public static string ListCollectionView InstrumentsView
{
get
{
return new ListCollectionView(Instruments);
}
}
Change your DataGridComboBoxColumn XAML as below...
<DataGridComboBoxColumn Header="Instrument" MinWidth="140"
ItemsSource="{x:Static ViewModel.InstrumentsView}">
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="IsEditable" Value="True"/>
<Setter Property="IsSynchronizedWithCurrentItem" Value=True" />
<Setter Property="SelectedItem" Value="{Binding SelectedInstrument, Mode=OneWayToSource}" /> <!-- Assuming that SelectedInstrument is string -->
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
Tell me if this works....
You can create your own ComboBox column type by subclassing DataGridBoundColumn. Compared to bathineni's solution of subclassing DataGridTemplateColumn the below solution has the benefit of better user experience (no double-tabbing) and you have more options to tune the column to your specific needs.
public class DataGridComboBoxColumn : DataGridBoundColumn {
public Binding ItemsSourceBinding { get; set; }
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) {
var textBox = new TextBlock();
BindingOperations.SetBinding(textBox, TextBlock.TextProperty, Binding);
return textBox;
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) {
var comboBox = new ComboBox { IsEditable = true };
BindingOperations.SetBinding(comboBox, ComboBox.TextProperty, Binding);
BindingOperations.SetBinding(comboBox, ComboBox.ItemsSourceProperty, ItemsSourceBinding);
return comboBox;
}
protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs) {
var comboBox = editingElement as ComboBox;
if (comboBox == null) return null;
comboBox.Focus(); // This solves the double-tabbing problem that Nick mentioned.
return comboBox.Text;
}
}
You can then use the component for example like this.
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding MyItems}">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<local:DataGridComboBoxColumn Header="Thingy" Binding="{Binding Thingy}"
ItemsSourceBinding="{Binding
RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}},
Path=Thingies}"/>
</DataGrid.Columns>
</DataGrid>
I got this solution by following this answer to a similar question.
Maybe it'll still be useful to someone. This solution allows to add new entered values to selection list and has no side effects while editing.
XAML:
<DataGridTemplateColumn Header="MyHeader" Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox IsEditable="True"
Text="{Binding MyTextProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="MyTextProperty"
SelectedValuePath="MyTextProperty"
ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}, Path=DataContext.SelectionList}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
ViewModel:
public class MyViewModel
{
public class MyItem : INotifyPropertyChanged {
private string myTextProperty;
public string MyTextProperty {
get { return myTextProperty; }
set { myTextProperty = value;
OnPropertyChanged("MyTextProperty"); }
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string prop = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
}
public ObservableCollection<MyItem> MyItems { get; set; }
public object SelectionList { get; set; }
}
CodeBehinde:
MyWindow.DataContext = MyViewModelInstance;
MyDataGrid.ItemsSource = MyItems;
// Before DataGrid loading and each time after new MyProperty value adding, you must execute:
MyViewModelInstance.SelectionList = MyViewModelInstance.MyItems.OrderBy(p => p.MyTextProperty).GroupBy(p => p.MyTextProperty).ToList();
I'm using the datagrid from wpf 4.0. This has a TemplateColumn containing a checkbox. The IsChecked property of the checkbox is set via binding.
The problem is that even if I specify the binding mode explicitly to be TwoWay, it works only in one direction.
I have to mention that the same code works perfectly in .net 3.5 with the datagrid from the wpf toolkit.
Please take a look at the .xaml and .cs contents.
Thanks in advance,
Roland
<Window.Resources>
<DataTemplate
x:Key="IsSelectedColumnTemplate">
<CheckBox
IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"
/>
</DataTemplate>
</Window.Resources>
<Grid>
<DataGrid
x:Name="dataGrid"
AutoGenerateColumns="false"
CanUserAddRows="False"
CanUserDeleteRows="False"
HeadersVisibility="Column"
ItemsSource="{Binding}"
>
<DataGrid.Columns>
<DataGridTemplateColumn
Header="Preselected"
x:Name="myIsSelectedColumn"
CellTemplate="{StaticResource IsSelectedColumnTemplate}"
CanUserSort="True"
SortMemberPath="Orientation"
Width="Auto"
/>
</DataGrid.Columns>
</DataGrid>
</Grid>
and the related .cs content:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ObservableCollection<DataObject> DataSource = new ObservableCollection<DataObject>();
DataSource.Add(new DataObject());
DataSource.Add(new DataObject());
dataGrid.ItemsSource = DataSource;
}
}
public class DataObject : DependencyObject
{
public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}
// Using a DependencyProperty as the backing store for IsSelected. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected", typeof(bool), typeof(DataObject), new UIPropertyMetadata(false, OnIsSelectedChanged));
private static void OnIsSelectedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
// this part is not reached
}
}
You set UpdateSourceTrigger=PropertyChanged in your Checkbox IsChecked binding in the datatemplate:
<CheckBox
IsChecked="{Binding Path=IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
/>
Here's the deal, the way the data grid works, is that it creates a data view and displays it instead of the original data, therefore when you simply bind a property in the CellTemplate it doesn't get propagated from the view to the data.
What you need to do is use the CellEditingTemplate so that the data grid knows when you're editing, and can propagate it to the data when done (or it can undo it if you cancel).
Here's the modified XAML for you:
<Window.Resources>
<DataTemplate x:Key="IsSelectedColumnTemplate">
<TextBlock Text="{Binding IsSelected}"/>
</DataTemplate>
<DataTemplate x:Key="IsSelectedColumnTemplateEditing">
<CheckBox IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"/>
</DataTemplate>
</Window.Resources>
...
<DataGridTemplateColumn
Header="Preselected"
x:Name="myIsSelectedColumn"
CellTemplate="{StaticResource IsSelectedColumnTemplate}"
CellEditingTemplate="{StaticResource IsSelectedColumnTemplateEditing}"
CanUserSort="True"
Width="Auto"
/>
...
What I need to accomplish is a ComboBox that shows People. When you expand the drop-down it shows FirstName and LastName, but when you select a person, the value shown at the combobox should be just the person's first name.
I have the following ItemTemplate:
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding LastName}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
What else should I do to display only the first name when one item is selected?
Thanks!
EDIT
Changed the question slightly: What if I have the person's picture and instead of showing just the first name when a person is selected, I want to show only the picture. In other words, how can I have two separate templates - one for the drop-down and one for the selected item?
Here's the solution:
<ComboBox>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<ContentControl x:Name="content" Content="{Binding}" ContentTemplate="{StaticResource ComplexTemplate}"/>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ComboBoxItem}}" Value="{x:Null}">
<Setter TargetName="content" Property="ContentTemplate" Value="{StaticResource SimpleTemplate}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Basically, you create one more layer of DataTemplate here. ComboBox'es ItemTemplate always stays the same. But the content inside that template adjusts to the condition you are interested in.
The trick to discriminate dropped-down combobox items against selected-area combobox item is that selected-area is not really enclosed in ComboBoxItem object, it's part of ComboBox control itself. So FindAncestor for ComboBoxItem returns null, which we use in the trigger above.
I got it. I just needed to add the following to my ComboBox:
IsEditable="True" IsReadOnly="True" TextSearch.TextPath="FirstName"
Put a Trigger on the DataTemplate. The trigger should check the IsSelected property (the DataTemplate will need a TargetType set for this to work). If it is selected, you can set the Visibility of your TextBlocks to Collapsed, and set the Visibility of the Image to Visible. Then do the opposite for the case that it is not selected.
Another option is to use ItemTemplateSelector instead of ItemTemplate. I've been using it the following way.
ComboBoxItemTemplateSelector derives from DataTemplateSelector and has two attached properties, SelectedTemplate and DropDownTemplate. Then we set the DataTemplates from Xaml like this
<ComboBox ItemsSource="{Binding Persons}"
ItemTemplateSelector="{StaticResource ComboBoxItemTemplateSelector}">
<ts:ComboBoxItemTemplateSelector.SelectedTemplate>
<DataTemplate>
<TextBlock Text="{Binding FirstName}" />
</DataTemplate>
</ts:ComboBoxItemTemplateSelector.SelectedTemplate>
<ts:ComboBoxItemTemplateSelector.DropDownTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding LastName}" />
</StackPanel>
</DataTemplate>
</ts:ComboBoxItemTemplateSelector.DropDownTemplate>
</ComboBox>
In SelectTemplate we check if the current container is wrapped in a ComboBoxItem and if it is, we return the DropDownTemplate. Otherwise we return SelectedTemplate.
public class ComboBoxItemTemplateChooser : DataTemplateSelector
{
#region SelectedTemplate..
#region DropDownTemplate..
public override DataTemplate SelectTemplate(object item,
DependencyObject container)
{
ComboBox parentComboBox = null;
ComboBoxItem comboBoxItem = container.GetVisualParent<ComboBoxItem>();
if (comboBoxItem == null)
{
parentComboBox = container.GetVisualParent<ComboBox>();
return ComboBoxItemTemplateChooser.GetSelectedTemplate(parentComboBox);
}
parentComboBox = ComboBox.ItemsControlFromItemContainer(comboBoxItem) as ComboBox;
return ComboBoxItemTemplateChooser.GetDropDownTemplate(parentComboBox);
}
}
A small demo project that uses this can be downloaded here: ComboBoxItemTemplateDemo.zip
I also made a short blog-post about this here: Different ComboBox ItemTemplate for dropdown. It also shows the other obvious way of doing the same thing but with properties instead of attached properties in ComboBoxItemTemplateSelector.
Oh, and GetVisualParent. Everyone seems to have their own implementations of this but anyway, here's the one I'm using
public static class DependencyObjectExtensions
{
public static T GetVisualParent<T>(this DependencyObject child) where T : Visual
{
while ((child != null) && !(child is T))
{
child = VisualTreeHelper.GetParent(child);
}
return child as T;
}
}
I used next approach
<UserControl.Resources>
<DataTemplate x:Key="SelectedItemTemplate" DataType="{x:Type statusBar:OffsetItem}">
<TextBlock Text="{Binding Path=ShortName}" />
</DataTemplate>
</UserControl.Resources>
<StackPanel Orientation="Horizontal">
<ComboBox DisplayMemberPath="FullName"
ItemsSource="{Binding Path=Offsets}"
behaviors:SelectedItemTemplateBehavior.SelectedItemDataTemplate="{StaticResource SelectedItemTemplate}"
SelectedItem="{Binding Path=Selected}" />
<TextBlock Text="User Time" />
<TextBlock Text="" />
</StackPanel>
And the behavior
public static class SelectedItemTemplateBehavior
{
public static readonly DependencyProperty SelectedItemDataTemplateProperty =
DependencyProperty.RegisterAttached("SelectedItemDataTemplate", typeof(DataTemplate), typeof(SelectedItemTemplateBehavior), new PropertyMetadata(default(DataTemplate), PropertyChangedCallback));
public static void SetSelectedItemDataTemplate(this UIElement element, DataTemplate value)
{
element.SetValue(SelectedItemDataTemplateProperty, value);
}
public static DataTemplate GetSelectedItemDataTemplate(this ComboBox element)
{
return (DataTemplate)element.GetValue(SelectedItemDataTemplateProperty);
}
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uiElement = d as ComboBox;
if (e.Property == SelectedItemDataTemplateProperty && uiElement != null)
{
uiElement.Loaded -= UiElementLoaded;
UpdateSelectionTemplate(uiElement);
uiElement.Loaded += UiElementLoaded;
}
}
static void UiElementLoaded(object sender, RoutedEventArgs e)
{
UpdateSelectionTemplate((ComboBox)sender);
}
private static void UpdateSelectionTemplate(ComboBox uiElement)
{
var contentPresenter = GetChildOfType<ContentPresenter>(uiElement);
if (contentPresenter == null)
return;
var template = uiElement.GetSelectedItemDataTemplate();
contentPresenter.ContentTemplate = template;
}
public static T GetChildOfType<T>(DependencyObject depObj)
where T : DependencyObject
{
if (depObj == null) return null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null) return result;
}
return null;
}
}
worked like a charm. Don't like pretty much Loaded event here but you can fix it if you want