WPF DataGrid Sync Column Widths - wpf

I've got two WPF Toolkit DataGrids, I'd like so that when the user resizes the first column in the first grid, it resizes the first column in the second grid. I've tried binding the width of the DataGridColumn in the second grid to the appropriate column in the first grid, but it doesn't work. I'd prefer to use all xaml, but I'm fine with using code behind as well.
<tk:DataGrid Width="100" Height="100">
<tk:DataGrid.Columns>
<tk:DataGridTextColumn x:Name="Column1" Width="50"/>
</tk:DataGrid.Columns>
</tk:DataGrid>
<tk:DataGrid Width="100" Height="100">
<tk:DataGrid.Columns>
<tk:DataGridTextColumn x:Name="Column1Copy" Width="{Binding Path=ActualWidth, ElementName=Column1}"/>
</tk:DataGrid.Columns>
</tk:DataGrid>
I also tried binding to Width instead of ActualWidth, but neither works.
Any help is greatly appreciated.

Well, I don't think that it is possible using straight XAML, but I still feel like it should because DataGridColumn does derive from DependencyObject. I did find a way to do it programatically though. I'm not thrilled about it, but it works:
DataGridColumn.WidthProperty.AddValueChanged(upperCol, delegate
{
if (changing) return;
changing = true;
mainCol.Width = upperCol.Width;
changing = false;
});
DataGridColumn.WidthProperty.AddValueChanged(mainCol, delegate
{
if (changing) return;
changing = true;
upperCol.Width = mainCol.Width;
changing = false;
});
public static void AddValueChanged(this DependencyProperty property, object sourceObject, EventHandler handler)
{
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(property, property.OwnerType);
dpd.AddValueChanged(sourceObject, handler);
}

You can use the DataGrid LayoutUpdated method to manipulate other objects regarding the column widths.
private void dataGrid1_LayoutUpdated(object sender, EventArgs e)
{
for(int i = 0 ; i < dataGrid1.Columns.Count && i < dataGrid2.Columns.Count ; ++i)
dataGrid2.Columns[i].Width = dataGrid1.Columns[i].ActualWidth;
}

I did a quick solution to this to using a attached behavior, Inspired by Ahmed answer above.
public class DataGridWidthSyncronizerBehavior
{
public static readonly DependencyProperty SyncronizeWidthWithProperty =
DependencyProperty.RegisterAttached("SyncronizeWidthWith",
typeof(DataGrid),
typeof(DataGridWidthSyncronizerBehavior),
new UIPropertyMetadata(null, SyncronizeWidthWithChanged));
public static void SetSyncronizeWidthWith(DependencyObject target, DataGrid value)
{
target.SetValue(SyncronizeWidthWithProperty, value);
}
public static DataGrid GetSyncronizeWidthWith(DependencyObject target)
{
return (DataGrid)target.GetValue(SyncronizeWidthWithProperty);
}
private static void SyncronizeWidthWithChanged(DependencyObject obj, DependencyPropertyChangedEventArgs dpargs)
{
if (!(obj is DataGrid sourceDataGrid))
return;
if (!(sourceDataGrid.GetValue(SyncronizeWidthWithProperty) is DataGrid targetDataGrid))
return;
void Handler(object sender, EventArgs e)
{
for (var i = 0; i < sourceDataGrid.Columns.Count && i < targetDataGrid.Columns.Count; ++i)
targetDataGrid.Columns[i].Width = sourceDataGrid.Columns[i].ActualWidth;
}
sourceDataGrid.LayoutUpdated -= Handler;
sourceDataGrid.LayoutUpdated += Handler;
}
}
XAML:
<DataGrid local:DataGridWidthSyncronizerBehavior.SyncronizeWidthWith="{Binding ElementName=SyncronizedHeaderGrid}">
<DataGrid.Columns>
<DataGridTextColumn Header="Header 1"
Binding="{Binding Item1}" />
<DataGridTextColumn Header="Header 2"
Binding="{Binding Item2}"/>
<DataGridTextColumn Header="Header 3"
Binding="{Binding Item3}"/>
</DataGrid.Columns>
</DataGrid>
<DataGrid x:Name="SyncronizedHeaderGrid">
<DataGrid.Columns>
<DataGridTextColumn Header="Header 1"
Binding="{Binding Item1}" />
<DataGridTextColumn Header="Header 2"
Binding="{Binding Item2}"/>
<DataGridTextColumn Header="Header 3"
Binding="{Binding Item3}"/>
</DataGrid.Columns>
</DataGrid>
The Second DataGrids, header and cell width, is now syncronized with the first grid header width.

I tried this:
<tk:DataGrid Width="100" Height="100" x:Name="Grid1" Grid.Column="0">
<tk:DataGrid.Columns>
<tk:DataGridTextColumn x:Name="Column1" Width="50"/>
</tk:DataGrid.Columns>
</tk:DataGrid>
<tk:DataGrid Width="100" Height="100" x:Name="Grid2" Grid.Column="1">
<tk:DataGrid.Columns>
<tk:DataGridTextColumn x:Name="Column1Copy" Width="{Binding Mode=TwoWay, Path=Columns[0].ActualWidth, ElementName=Grid1}"/>
</tk:DataGrid.Columns>
</tk:DataGrid>
However, It looks like since DataGridColumns do not derive from FrameworkElement but instead derive from DependencyObject, binding in this manner is not available.

If you want to bind column Width property in XAML, in 2 DataGrid's than you have to do the following.
In the first DataGrid name the DataGridTextColumn:
<DataGrid>
<DataGrid.Columns>
<DataGridTextColumn x:Name="Col1"/>
</DataGrid.Columns>
</DataGrid>
In the second DataGrid add a DiscreteObjectKeyFrame pointing to the above mentioned column as a resource, and use the following Binding to Width property on the DataGridTextColumn you want to "link":
<DataGrid>
<DataGrid.Resources>
<DiscreteObjectKeyFrame x:Key="proxyCol1" Value="{Binding ElementName=Col1}"/>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Width="{Binding Path=Value.Width, Mode=TwoWay, Source={StaticResource proxyCol1}}"/>
</DataGrid.Columns>
</DataGrid>

I found a solution to this problem, and an extra cool solution :-)
You can download the WPF toolkit and get the code of the DataGrid.
Once you have the code all you have to do is change the DataGridColumn class to inherit FrameworkElement instead of DependencyObject.
Once you do so - you are left with only one problem, the DataContext of the column would not be initialized since the column is not part of the logical tree, adding it to the logical tree would solve this.
You can do it like this:
Where the OnColumnInitialization is:
private void OnColumnInitialization(object sender, EventArgs e)
{
AddLogicalChild(sender);
}
Now that it is part of the logical tree you have the same data context and you can use binding on the
Width property.
If all are bound to the same Width - you have a complete sync of the Width of your columns.
This worked for me :-)
Gili

Related

WPF MVVM DataGrid RowDetails with different ItemsSource

I have three DataGrids set up as individual UserControls with each ItemsSource bound to a different DependencyProperty in my ViewModel.
When a row is selected in the first DataGrid, the other two populate with information relevant to the selected row. Though this works fine, I want the second and third DataGrid to be displayed in the RowDetailsTemplate of the first DataGrid.
The issue I am having is that the ItemsSource of the parent DataGrid is overriding that of the other two, so they are not populating. I have tried solutions posted on many other similar questions but none have solved my problem. My code is below, I'm hoping that I've just missed something obvious but any help would be appreciated!
Main Datagrid
<DataGrid x:Name="PostDataGrid"
ItemsSource="{Binding WSEDriverList}"
SelectedItem="{Binding SelectedWSEDriverPOST}"
Style="{DynamicResource MainDataGridStyle}"
Margin="0,0,0,0"
Grid.Row="1" >
<DataGrid.Columns>
<DataGridTextColumn Header="HesId" Binding="{Binding HESID, Mode=OneWay}" Width="50"/>
<DataGridTextColumn Header="WseId" Binding="{Binding WSEID, Mode=OneWay}" Width="50"/>
<DataGridTextColumn Header="API" Binding="{Binding API, Mode=OneWay}" Width="198" />
<DataGridTextColumn Header="Request Date" Binding="{Binding Path=REQUEST_DATE, Mode=OneWay, StringFormat=dd/MM/yyyy HH:mm:ss}" Width="125"/>
<DataGridTextColumn Header="Result Date" Binding="{Binding Path=RESULT_DATE, Mode=OneWay, StringFormat=dd/MM/yyyy HH:mm:ss}" Width="125"/>
<DataGridTextColumn Header="Return Code" Binding="{Binding RETURN_CODE, Mode=OneWay, StringFormat=0}" Width="80" />
<DataGridTextColumn Header="Status" Binding="{Binding STATUS, Mode=OneWay, StringFormat=0}" Width="70" />
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate DataType="{x:Type viewmodel:WSEAuditViewModel}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Expander Header="NOTIFICATION" Grid.Row="0">
<ItemsControl ItemsSource="{Binding WSEDriverGETResult}">
<usercontrol:NOTIFICATIONUserControl/>
</ItemsControl>
</Expander>
<Expander Header="GET" Grid.Row="1">
<ItemsControl ItemsSource="{Binding WSEDriverGETResult}">
<usercontrol:GETUserControl/>
</ItemsControl>
</Expander>
</Grid>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
DataGrid 2 (NOTIFICATIONUserControl)
<DataGrid x:Name="NotificationDG"
ItemsSource="{Binding NotificationResult}"
DataGrid 3 (GETUserControl)
<DataGrid x:Name="GetDG"
ItemsSource="{Binding WSEDriverGETResult}"
Edit
Here is the code for my DependencyProperties bound to my DataGrid.
//----POST result list from WSE_DRIVER-----
public List<WSE_DRIVER> WSEDriverList
{
get { return (List<WSE_DRIVER>)GetValue(WSEDriverListProperty); }
set { SetValue(WSEDriverListProperty, value); }
}
public static readonly DependencyProperty WSEDriverListProperty =
DependencyProperty.Register("WSEDriverList",
typeof(List<WSE_DRIVER>),
typeof(WSEAuditViewModel),
new PropertyMetadata(WSEDriverListChanged_Callback));
private static void WSEDriverListChanged_Callback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
WSEAuditViewModel wse_win = (WSEAuditViewModel)d;
if (wse_win.WSEDriverList.Count > 0)
{
wse_win.SelectedWSEDriverPOST = wse_win.WSEDriverList.First();
}
}
//----Selected POST WSE_Driver----
public WSE_DRIVER SelectedWSEDriverPOST
{
get { return (WSE_DRIVER)GetValue(SelectedWSEDriverPOSTProperty); }
set { SetValue(SelectedWSEDriverPOSTProperty, value); }
}
public static readonly DependencyProperty SelectedWSEDriverPOSTProperty =
DependencyProperty.Register("SelectedWSEDriverPOST",
typeof(WSE_DRIVER),
typeof(WSEAuditViewModel),
new PropertyMetadata(SelectedWSEDriverPOSTChanged_Callback));
private static void SelectedWSEDriverPOSTChanged_Callback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//Gets list and assigns to WSEDriverGETResult and NotificationResult
}
//----GET result from WSE_Driver----
public List<WSE_DRIVER> WSEDriverGETResult
{
get { return (List<WSE_DRIVER>)GetValue(WSEDriverGETResultProperty); }
set { SetValue(WSEDriverGETResultProperty, value); }
}
public static readonly DependencyProperty WSEDriverGETResultProperty =
DependencyProperty.Register("WSEDriverGETResult",
typeof(List<WSE_DRIVER>),
typeof(WSEAuditViewModel),
new PropertyMetadata(WSEDriverGETResultChanged_Callback));
private static void WSEDriverGETResultChanged_Callback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
}
//----Notification Info----
public List<SPEC_NOTIFICATION> NotificationResult
{
get { return (List<SPEC_NOTIFICATION>)GetValue(NotificationResultProperty); }
set { SetValue(NotificationResultProperty, value); }
}
public static readonly DependencyProperty NotificationResultProperty =
DependencyProperty.Register("NotificationResult",
typeof(List<SPEC_NOTIFICATION>),
typeof(WSEAuditViewModel),
new UIPropertyMetadata(NotificationResultChanged_Callback));
private static void NotificationResultChanged_Callback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
}
Since the NotificationResult and WSEDriverGETResult properties belong to the same class as the WSEDriverList property, you should be able to use a {RelativeSource} to bind to the DataContext of the parent DataGrid:
<DataGrid x:Name="NotificationDG" ItemsSource="{Binding DataContext.NotificationResult,
RelativeSource={RelativeSource AncestorType=DataGrid}}"></DataGrid>
<DataGrid x:Name="GetDG" ItemsSource="{Binding DataContext.WSEDriverGETResult,
RelativeSource={RelativeSource AncestorType=DataGrid}}"></DataGrid>

wpf DataGrid RowDetailsTemplate Scroll

<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;
}

WPF-MVVM: Handeling Event from within DataGrid Style

I develop Prism+MEF application based on WPF+MVVM, where there are many DataGrids so i build DataGridStyle to be applied for all DataGrids in all modules. The style add Filter Text Box in the Columns Headers, and i used MVVM Light EventToCommand to trigger TextChanged event when the Text Box text is changed like this: (this code exist in DataGridStyle Resource Dictionary)
<TextBox x:Name="filterTextBox"
HorizontalAlignment="Right" MinWidth="25" Height="Auto"
OpacityMask="Black" Visibility="Collapsed"
Text=""
TextWrapping="Wrap" Grid.Column="0" Grid.ColumnSpan="1">
<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<cmd:EventToCommand Command="{Binding DataContext.TextChangedCommand}"
PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
Then i handled the TextChangedCommand in ViewModel (pertaining to View that contains the Data Grid)using Attached property:
#region TextChangedCommand----------------------------------------------------------------------------------------------
static ICommand command; //1
public static ICommand GetTextChangedCommand(DependencyObject obj)
{
return (ICommand)obj.GetValue(TextChangedCommandProperty);
}
public static void SetTextChangedCommand(DependencyObject obj, ICommand value)
{
obj.SetValue(TextChangedCommandProperty, value);
}
// Using a DependencyProperty as the backing store for TextChangedCommand. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TextChangedCommandProperty =
DependencyProperty.RegisterAttached("TextChangedCommand",
typeof(ICommand),
typeof(SubsystemAllViewModel),
new UIPropertyMetadata(null, CommandChanged));
static void CommandChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var fe = obj as FrameworkElement;
command = e.NewValue as ICommand;
fe.AddHandler(TextBox.TextChangedEvent, new TextChangedEventHandler(ExecuteCommand));
}
static void ExecuteCommand(object sender, TextChangedEventArgs e)
{
//Some Code
}
#endregion
And the View that contains the Grid:
<DataGrid ItemsSource="{Binding Subsystems,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding Path=SelectedSubsystem, Mode=TwoWay}"
Name="SubsystemAllDataGrid"
Style="{StaticResource DataGridStyle}"
Grid.Row="2">
<DataGrid.Columns>
<DataGridTextColumn Header="Serial" Binding="{Binding Path=Serial, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="Type" Binding="{Binding Path=Type, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="System" Binding="{Binding Path=System, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="SubsystemNo" Binding="{Binding Path=SubsystemNo, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="Description" Binding="{Binding Path=Description, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="Scope" Binding="{Binding Path=Scope, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="Area" Binding="{Binding Path=Area, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="Priority" Binding="{Binding Path=Priority, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="DossierLocation" Binding="{Binding Path=DossierLocation, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="Parts" Binding="{Binding Path=Parts, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="Status" Binding="{Binding Path=Status, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="StatusDate" Binding="{Binding Path=StatusDate, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="MCDate" Binding="{Binding Path=MCDate, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="PlnMCDate" Binding="{Binding Path=PlnMCDate, Mode=TwoWay}"></DataGridTextColumn>
<DataGridTextColumn Header="Remark" Binding="{Binding Path=Remark, Mode=TwoWay}"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
The problems are:
When i enter filter text in the text box in one of the columns headers of the data grid nothing happen and break points at following points not hit:
1-GetTextChangedCommand and SetTextChangedCommand
2-The CommandChanged() method.
i'm new to wpf so i'm sure there are faults in WPF or C# code..., so please help me fixing these faults.
Note: i don't use code behind.
Thanks in advance
Looks like your Command-Binding doesnt work. Try the following:
Command="{Binding Path=TextChangedCommand}"
or
Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}} Path=DataContext.TextChangedCommand}"
Breakpoints not hit makes me thing binding issue.
The datacontext for you command needs to be set to where your command is defined. SO if your command is defined in your viewmodel, you need to use the following but set the ansestor type to the type of object that binds to your viewmodel. This is usually your view, not the datagrid:
Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type ViewType}} Path=DataContext.TextChangedCommand}"
So if your view is a window or user control then change the ViewType to the respective type.
You can also see if there are any binding errors generated for your command in the output window when you run your app.
EDIT 1:
Don't use dependency property, use relay command from MVVM-Light in your view model:
this.YourRelayCommand = new RelayCommand( ExecuteYourCommand, CanYourCommandExecute);
public RelayCommand YourRelayCommand{ get; private set; }
public bool CanYourCommandExecute() {
}
public void ExecuteYourCommand() {
//TODO: Do code here
}

WPF DataGridTemplateColumn Visibility Binding under MVVM

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?

Can I share DataGrid.Columns amongst DataGrid tables

I have 3 datagrids that share the same data type. I'd like to configure the column binding once and have the 3 datagrids share the resource.
e.g.
<DataGrid Grid.Row="1" x:Name="primaryDG" ItemsSource="{Binding Path=dgSource AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Width="Auto" Header="Column 1" Binding="{Binding Path=Col1}"/>
<DataGridTextColumn Width="Auto" Header="Column 2" Binding="{Binding Path=Col2}"/>
<DataGridTextColumn Width="Auto" Header="Column 3" Binding="{Binding Path=Col3}"/>
<DataGridTextColumn Width="Auto" Header="Column 4" Binding="{Binding Path=Col4}"/>
</DataGrid.Columns>
</DataGrid>
Is there a way to set the ItemsSource for each DataGrid, then use a datatemplate or controltemplate to get the columns?
Yes... two ways. One you can simply add a style for DataGrid that sets the columns like this...
<Style x:Key="MyColumnDefsStyle" x:Shared="True" TargetType="DataGrid">
<Setter Property="Columns">
<Setter.Value>
<DataGridTextColumn Width="Auto" Header="Column 1" Binding="{Binding Path=Col1}"/>
<DataGridTextColumn Width="Auto" Header="Column 2" Binding="{Binding Path=Col2}"/>
<DataGridTextColumn Width="Auto" Header="Column 3" Binding="{Binding Path=Col3}"/>
<DataGridTextColumn Width="Auto" Header="Column 4" Binding="{Binding Path=Col4}"/>
</Setter.Value>
</Setter>
</Style>
<DataGrid Style="{StaticResource MyColumnDefsStyle}" ItemsSource="{Binding Foo1}" />
<DataGrid Style="{StaticResource MyColumnDefsStyle}" ItemsSource="{Binding Foo2}" />
<DataGrid Style="{StaticResource MyColumnDefsStyle}" ItemsSource="{Binding Foo3}" />
That works but represents a problem if you are applying it to multiple grids that themselves may already be using a style.
In that case, the other, more flexible way works better. This however requires creating a XAML-friendly classes to represent an ObservableCollection<DataGridColumn> (although you technically only said columns, I like to be complete myself so I'd also do one for the rows) Then add them in a place you can reference in the XAML namespaces. (I call mine xmlns:dge for 'DataGridEnhancements') You then use it like this:
In the code somwhere (I'd make it accessible app-wide)...
public class DataGridRowsCollection : ObservableCollection<DataGridRow>{}
public class DataGridColumnsCollection : ObservableCollection<DataGridColumn>{}
Then in the resources...
<dge:DataGridColumnsCollection x:Key="MyColumnDefs" x:Shared="True">
<DataGridTextColumn Width="Auto" Header="Column 1" Binding="{Binding Path=Col1}"/>
<DataGridTextColumn Width="Auto" Header="Column 2" Binding="{Binding Path=Col2}"/>
<DataGridTextColumn Width="Auto" Header="Column 3" Binding="{Binding Path=Col3}"/>
<DataGridTextColumn Width="Auto" Header="Column 4" Binding="{Binding Path=Col4}"/>
</dge:DataGridColumnsCollection>
And finally in the XAML...
<DataGrid Columns="{StaticResource MyColumnDefs}" ItemsSource="{Binding Foo1}" />
<DataGrid Columns="{StaticResource MyColumnDefs}" ItemsSource="{Binding Foo2}" />
<DataGrid Columns="{StaticResource MyColumnDefs}" ItemsSource="{Binding Foo3}" />
HTH,
Mark
EDIT:
Since you cannot set the DataGrid.Columns property, you need to enhance your DataGridView (as mentioned in the comments). Here is the code for an EnhancedDataGrid:
public class EnhancedDataGrid : DataGrid
{
//the dependency property for 'setting' our columns
public static DependencyProperty SetColumnsProperty = DependencyProperty.Register(
"SetColumns",
typeof (ObservableCollection<DataGridColumn>),
typeof (EnhancedDataGrid),
new FrameworkPropertyMetadata
{
DefaultValue = new ObservableCollection<DataGridColumn>(),
PropertyChangedCallback = EnhancedDataGrid.SetColumnsChanged,
AffectsRender = true,
AffectsMeasure = true,
AffectsParentMeasure = true,
IsAnimationProhibited = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
});
//callback to reset the columns when our dependency property changes
private static void SetColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var datagrid = (DataGrid) d;
datagrid.Columns.Clear();
foreach (var column in (ObservableCollection<DataGridColumn>)e.NewValue)
{
datagrid.Columns.Add(column);
}
}
//The dependency property wrapper (so that you can consume it inside your xaml)
public ObservableCollection<DataGridColumn> SetColumns
{
get { return (ObservableCollection<DataGridColumn>) this.GetValue(EnhancedDataGrid.SetColumnsProperty); }
set { this.SetValue(EnhancedDataGrid.SetColumnsProperty, value); }
}
}
Now you could set the columns with the SetColumns dependency property created in your CustomControl:
<custom:EnhancedDataGrid SetColumns="{StaticResource MyColumnDefs}" ItemsSource="{Binding Foo1}" />
<custom:EnhancedDataGrid SetColumns="{StaticResource MyColumnDefs}" ItemsSource="{Binding Foo2}" />
<custom:EnhancedDataGrid SetColumns="{StaticResource MyColumnDefs}" ItemsSource="{Binding Foo3}" />
This answer is based on MarquelV's solution. In his answer (and in the comments) he mentions a Custom Control named EnhancedDataGrid, where he provides logic to set the DataGrid.Columns property. Here is the code for an EnhancedDataGrid:
public class EnhancedDataGrid : DataGrid
{
//the dependency property for 'setting' our columns
public static DependencyProperty SetColumnsProperty = DependencyProperty.Register(
"SetColumns",
typeof (ObservableCollection<DataGridColumn>),
typeof (EnhancedDataGrid),
new FrameworkPropertyMetadata
{
DefaultValue = new ObservableCollection<DataGridColumn>(),
PropertyChangedCallback = EnhancedDataGrid.SetColumnsChanged,
AffectsRender = true,
AffectsMeasure = true,
AffectsParentMeasure = true,
IsAnimationProhibited = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
});
//callback to reset the columns when our dependency property changes
private static void SetColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var datagrid = (DataGrid) d;
datagrid.Columns.Clear();
foreach (var column in (ObservableCollection<DataGridColumn>)e.NewValue)
{
datagrid.Columns.Add(column);
}
}
//The dependency property wrapper (so that you can consume it inside your xaml)
public ObservableCollection<DataGridColumn> SetColumns
{
get { return (ObservableCollection<DataGridColumn>) this.GetValue(EnhancedDataGrid.SetColumnsProperty); }
set { this.SetValue(EnhancedDataGrid.SetColumnsProperty, value); }
}
}
Now you could set the columns with the SetColumns dependency property created in your CustomControl:
<custom:EnhancedDataGrid SetColumns="{StaticResource MyColumnDefs}" ItemsSource="{Binding Foo1}" />
<custom:EnhancedDataGrid SetColumns="{StaticResource MyColumnDefs}" ItemsSource="{Binding Foo2}" />
<custom:EnhancedDataGrid SetColumns="{StaticResource MyColumnDefs}" ItemsSource="{Binding Foo3}" />
You can create a custom control just to wrap the data grid and pass data to it. The control will set the data on a grid.

Resources