wpf DataGrid RowDetailsTemplate Scroll - wpf

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

Related

How to get the DataGridTextColumn sender's datagrid parent

Hi I have a datagrid and the DataGridTextColumn shown in code below:
<DataGridTextColumn Header="" Width="1*" Binding="{Binding FORECAST_MIN, UpdateSourceTrigger=PropertyChanged,NotifyOnTargetUpdated=True}" >
<DataGridTextColumn.EditingElementStyle>
<Style TargetType="{x:Type TextBox}">
<EventSetter Event="LostFocus" Handler="fMin_LostFocus" />
</Style>
</DataGridTextColumn.EditingElementStyle>
Now in the LostFocus event, I'd like to get the parent datagrid from the sender. Code
private void fMin_LostFocus(object sender, RoutedEventArgs e)
{
//Get the datagrid parent
}
Is there a easy way to do so? Thank you. Something like adding a Tag?
#
Both Jeff and OptimusPrime's answers work. It only allows me to choose one answer.
Jeff's answer should work.
Since you mentioned "Tag". This might be another way to go? Probably not the most elegant way though.
<DataGridTemplateColumn Header="">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding FORECAST_MIN, UpdateSourceTrigger=PropertyChanged,NotifyOnTargetUpdated=True}" Tag="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}}" LostFocus="fMin_LostFocus"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
and in your code:
private void fMin_LostFocus(object sender, RoutedEventArgs e)
{
var tb = (TextBox)sender;
DataGrid parentDataGrid = (DataGrid)tb.Tag;
}
You have to traverse the visual tree until you find the proper parent.
DependencyObject depObj = sender as DependencyObject;
while (depObj != null && !(depObj is DataGrid)) {
depObj = VisualTreeHelper.GetParent (depObj);
}
DataGrid dg = (DataGrid) depObj;

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?

Silverlight Checkbox inside datagrid causing Trouble

Hi all i came across a problem with the checkboxes inside the datagrid.
First let me post my code of what i have achieved and then i will tell what's causing the trouble
This is the code of my datagrid inside a child window
<Controls:DataGrid x:Name="dgAthlete" Height="Auto" Width="Auto"
IsReadOnly="True" AutoGenerateColumns="False"
HorizontalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
ItemsSource="{Binding Data, ElementName=dds}"
>
<Controls:DataGrid.Columns>
<Controls:DataGridTemplateColumn Header="CheckBoxColumn">
<Controls:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox x:Name="cbAddAthlete" IsChecked="{Binding IsAdded}" Tag="{Binding}"
IsEnabled="True" Checked="cbAddAthlete_Checked" Unchecked="cbAddAthlete_Unchecked" />
</DataTemplate>
</Controls:DataGridTemplateColumn.CellTemplate>
<Controls:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Path=IsAdded,Mode=TwoWay}"/>
</DataTemplate>
</Controls:DataGridTemplateColumn.CellEditingTemplate>
</Controls:DataGridTemplateColumn>
<Controls:DataGridTemplateColumn>
<Controls:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image Source="{Binding ImageFileName}"
Width="25" Height="25"
HorizontalAlignment="Stretch" />
</DataTemplate>
</Controls:DataGridTemplateColumn.CellTemplate>
</Controls:DataGridTemplateColumn>
<Controls:DataGridTextColumn Header="FirstName" Binding="{Binding FirstName}" />
<Controls:DataGridTextColumn Header="MiddleName" Binding="{Binding MiddleName}"/>
<Controls:DataGridTextColumn Header="LastName" Binding="{Binding LastName}"/>
<Controls:DataGridTextColumn Header="Email" Binding="{Binding Email}" />
<Controls:DataGridTextColumn Header="DOB" Binding="{Binding BirthDate}"/>
<Controls:DataGridTextColumn Header="Phone" Binding="{Binding PhoneNumber}"/>
<Controls:DataGridTextColumn Header="Website" Binding="{Binding WebSite}"/>
<Controls:DataGridTextColumn Header="Team" Binding="{Binding TeamName}"/>
<Controls:DataGridTextColumn Header="Club" Binding="{Binding ClubName}"/>
<Controls:DataGridTextColumn Header="County" Binding="{Binding CountyName}"/>
</Controls:DataGrid.Columns>
</Controls:DataGrid>
<Controls:DataPager x:Name="dpAthlete" PageSize="4"
HorizontalAlignment="Stretch" Source="{Binding Data, ElementName=dds}"
Width="Auto"/>
</StackPanel>
<Button x:Name="CancelButton" Content="Cancel" Click="CancelButton_Click" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,0,0" Grid.Row="1" />
<Button x:Name="OKButton" Content="OK" Click="OKButton_Click" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,79,0" Grid.Row="1" />
The code behind for this is
public partial class AddAthletesToCompetition : ChildWindow
{
public ObservableCollection<Athlete> Athletes { get; set; }
public int CompetitionId { get; set; }
private PagedCollectionView pvcAthlete;
public AddAthletesToCompetition(int competitionId)
{
InitializeComponent();
CompetitionId = competitionId;
LoadAthlete();
Athletes = new ObservableCollection<Athlete>();
}
private void OKButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = true;
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
}
private void LoadAthlete()
{
var context = new PublicServiceClient();
context.GetAthletesNotInCompetitionCompleted += context_GetAthletesNotInCompetitionCompleted;
context.GetAthletesNotInCompetitionAsync(CompetitionId);
}
void context_GetAthletesNotInCompetitionCompleted(object sender, GetAthletesNotInCompetitionCompletedEventArgs e)
{
if (e.Result != null)
{
pvcAthlete = new PagedCollectionView(e.Result.ToList());
dgAthlete.ItemsSource = pvcAthlete;
dpAthlete.Source = pvcAthlete;
}
}
//Checkbox Checked Event Hanlder
private void cbAddAthlete_Checked(object sender, RoutedEventArgs e)
{
var athlete = ((CheckBox)sender).Tag as AthleteBO;
if (athlete != null)
{
var ath = new Athlete();
ath.AthleteId = athlete.AthleteId;
Athletes.Add(ath);
}
}
//CheckBox unchecked Event Handler
private void cbAddAthlete_Unchecked(object sender, RoutedEventArgs e)
{
var athlete = ((CheckBox)sender).Tag as AthleteBO;
if (athlete != null)
{
var item = Athletes.First(i => i.AthleteId == athlete.AthleteId);
Athletes.Remove(item);
}
}
}
As you can see i am using paging everything works fine i.e check and uncheck events for the checkbox work fine when we are on the first page but let's say you checked the first and the second item in the grid on the first page , now as soon as i go to the next page what my grid is doing its retaining the previous view and checking by default the first and the second item on the second page which is not expected behaviour
I went through various post on this issue and found that the problem is that the view is injected by the datagrid but somehow i couldn't find any solution.
What other's were talking about was refreshing the observable collection and again binding that to my grid.
So i was a little confused as where to bind that and when to update the observable collection.
I have posted whole of the code and whenever you reply please mention where exactly should i do the changes for this to work.
For a note i am using WCF and EF , so the list of athletes which i have here i will be sending this list to the WCF service where using EF it will be inserted into the database.
I know it's not a bug in Silverlight but this added feature of grid virtualization is causing a trouble with me currently so there should aslo be some solution to this problem.
Thanks
Add a IsSelected bool property to your entity and bind the checkbox control to it. This code sample looks like you are using .NET RIA services and the DomainDataSource (ItemsSource="{Binding Data, ElementName=dds}") control, not WCF? I have used this solution with WCF, but not in a paged setup. Give it a try and let us know how it turns out,
I was binding my combobox but was still getting the problem, till I started using two way binding, seems to be gone now

WPF DataGridTemplateColumn. Am I missing something?

<data:DataGridTemplateColumn Header="Name">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}">
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding Name}">
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
It's clear example of Template column, right? What could be wrong with that?
So, here is the thing - when a user navigates through DataGrid with hitting TAB-key, it needs to hit the TAB twice(!) to be able to edit text in TextBox. How could I make it editable as soon as the user gets the column focus, I mean even if he just starts typing?
Ok. I found a way - into Grid.KeyUp() I put the code below:
if (Grid.CurrentColumn.Header.ToString() == "UserName")
{
if (e.Key != Key.Escape)
{
Grid.BeginEdit();
// Simply send another TAB press
if (Keyboard.FocusedElement is Microsoft.Windows.Controls.DataGridCell)
{
var keyEvt = new KeyEventArgs(Keyboard.PrimaryDevice, Keyboard.PrimaryDevice.ActiveSource, 0, Key.Tab) { RoutedEvent = Keyboard.KeyDownEvent };
InputManager.Current.ProcessInput(keyEvt);
}
}
}
your issue stems from the fact that each cell puts its editor in a content control which first receives focus, then you have to tab once again to the editor. If you have a look at the code for DataGridTemplateColumn in the GenerateEditingElement method it calls a method LoadTemplateContent which does this:
private FrameworkElement LoadTemplateContent(bool isEditing, object dataItem, DataGridCell cell)
{
DataTemplate template = ChooseCellTemplate(isEditing);
DataTemplateSelector templateSelector = ChooseCellTemplateSelector(isEditing);
if (template != null || templateSelector != null)
{
ContentPresenter contentPresenter = new ContentPresenter();
BindingOperations.SetBinding(contentPresenter, ContentPresenter.ContentProperty, new Binding());
contentPresenter.ContentTemplate = template;
contentPresenter.ContentTemplateSelector = templateSelector;
return contentPresenter;
}
return null;
}
see how it creates a new content presenter to put the template in. Other people have dealt with this problem in a variety of ways, I derive my own column type to deal with this stuff. (so i dont create an extra element or set the content presenter to not receive focus) In this example they are using focus manager to deal with the same issue (i havent tested this code)
<tk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<Grid FocusManager.FocusedElement="{Binding ElementName=txt1}">
<TextBox Name="txt1" Text="{Binding XPath=#ISBN}"
BorderThickness="0" GotFocus="TextBox_GotFocus"/>
</Grid>
</DataTemplate>
</tk:DataGridTemplateColumn.CellEditingTemplate>
If you have a user control as your editor then you can use the pattern with the focus manager or use an event handler for the OnLoaded event.
The issue that you faced is that the control (e.g. TextBox) within the DataGridTemplateColumn is contained within a DataGridCell. By default the DataGridCell has tab-stop functionality. Thus the reason for having to hit TAB twice to get focus to your TextBox control. The solution is to disable the tab-stop functionality for the DataGridCell. This can be done via a style for the DataGridCell.
Here's the solution:
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="KeyboardNavigation.IsTabStop" Value="False" />
</Style>
Here is my approach. Its very close to #Nalin Jayasuriya answer, but I didn't want to create a style. Also this solution selects the text in the TextBox. Anyway - the XAML for the hole DataGrid looks like this.
<DataGrid Name="TextBlockDataGrid" ItemsSource="{Binding Path=Rows}" Style="{StaticResource DefaultSettingsDataGrid}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Text}" IsReadOnly="True"/>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="KeyboardNavigation.IsTabStop" Value="False"/>
</Style>
</DataGridTemplateColumn.CellStyle>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="{Binding ErrorBorderThickness}" BorderBrush="{Binding ErrorBorderBrush}">
<TextBox Text="{Binding UserText, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
HorizontalAlignment="Right"
GotKeyboardFocus="TextBox_GotKeyboardFocus"
PreviewMouseDown="TextBox_PreviewMouseDown"
Style="{StaticResource DefaultTextBox}"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
And the code-behind.
private void TextBox_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
try
{
((TextBox)sender).SelectAll();
}
catch (Exception ex) { GlobalDebug.debugForm.WriteText(ex); }
}
private void TextBox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
try
{
// If its a triple click, select all text for the user.
if (e.ClickCount == 3)
{
((TextBox)sender).SelectAll();
return;
}
// Find the TextBox
DependencyObject parent = e.OriginalSource as UIElement;
while (parent != null && !(parent is TextBox))
{
parent = System.Windows.Media.VisualTreeHelper.GetParent(parent);
}
if (parent != null)
{
if (parent is TextBox)
{
var textBox = (TextBox)parent;
if (!textBox.IsKeyboardFocusWithin)
{
// If the text box is not yet focussed, give it the focus and
// stop further processing of this click event.
textBox.Focus();
e.Handled = true;
}
}
}
}
catch (Exception ex) { GlobalDebug.debugForm.WriteText(ex); }
}
For a more info, have a look at my blog:
http://blog.baltz.dk/post/2014/11/28/WPF-DataGrid-set-focus-and-mark-text
My approach is to use a TriggerAction which sets the focus to the template element you want when it loads.
The trigger is very simple:
public class TakeFocusAndSelectTextOnVisibleBehavior : TriggerAction<TextBox>
{
protected override void Invoke(object parameter)
{
Dispatcher.BeginInvoke(
DispatcherPriority.Loaded,
new Action(() =>
{
AssociatedObject.Focus();
AssociatedObject.SelectAll();
}));
}
}
The DataTemplate looks like this:
<DataTemplate>
<TextBox Text="{Binding Path=Price, Mode=TwoWay}"
MinHeight="0"
Padding="1,0"
Height="20">
<Interactivity:Interaction.Triggers>
<Interactivity:EventTrigger EventName="Loaded">
<Behaviors:TakeFocusAndSelectTextOnVisibleBehavior />
</Interactivity:EventTrigger>
</Interactivity:Interaction.Triggers>
</TextBox>
</DataTemplate>
You can write other triggers for other element types.

WPF DataGrid Sync Column Widths

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

Resources