WPF DataGridTemplateColumn. Am I missing something? - wpf

<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.

Related

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

how to access the label inside datatemplate

hello everybody i have a listbox within which is a datatemplate.Inside it is checkbox,textbox,label...Wat i want is to get the value of the label wen the checkbox is unchecked? or any alternative as to how to access the label value but only wen the checkbox is unselected............PLease help me out.
the code is as
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Name="sp" Orientation="Horizontal" Margin="3,3,3,3" >
<CheckBox Name="chkSubject" IsChecked="{Binding RelativeSource{RelativeSource AncestorType={x:Type ListBoxItem}}, Path=IsSelected}" VerticalAlignment="Center" Margin="0,0,4,0" Unchecked="chkSubject_Unchecked">
<TextBlock FontSize="11" Text="{Binding subject_name}" />
</CheckBox>
<Label Name="lbl_idOfSub" Content="{Binding subject_id}" Visibility="Visible">
</Label>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
Since you're using binding on label, I'd go for accessing subject_id from the object the datatemplate is describing. Like this:
var subjectId = dataBoundItem.subject_id;
That's the correct way to go with MVVM and bindings.
UPDATE:
Here's the basic MVVM approach to solving this problem. First of all, I've cleaned up a bit your listbox declaration and added a trigger that sets IsSelected binding:
<ListBox ItemsSource="{Binding}">
<ListBox.Resources>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Name="sp" Orientation="Horizontal" Margin="3,3,3,3" >
<CheckBox Name="chkSubject" IsChecked="{Binding IsSelected}" VerticalAlignment="Center" Margin="0,0,4,0" Unchecked="chkSubject_Unchecked_1">
<TextBlock FontSize="11" Text="{Binding SubjectName}" />
</CheckBox>
<Label Name="lbl_idOfSub" Content="{Binding SubjectId}" Visibility="Visible"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Here, whenever value IsSelected on individual ListBoxItem changes, the "IsSelected" binding of the viewModel is changed. Here's the model:
public class SelectableItem : INotifyPropertyChanged
{
private string _subjectId;
private bool _isSelected;
private string _subjectName;
public string SubjectId
{
get { return _subjectId; }
set { _subjectId = value; OnPropertyChanged("SubjectId"); }
}
public bool IsSelected
{
get { return _isSelected; }
set { _isSelected = value; OnPropertyChanged("IsSelected"); }
}
public string SubjectName
{
get { return _subjectName; }
set { _subjectName = value; OnPropertyChanged("SubjectName"); }
}
// .. INotifyPropertyChangedImplementation
Your IsSelected will be set to true whenever relevant item is selected and to false whenever it is unselected. You may put your code in to the "set" item of the "IsSelected" property and check (value == false) and execute necessary piece of code as you see fit. This would be MVVM approach to the matter.
Using the event, you can do as follows:
private void chkSubject_Unchecked_1(object sender, RoutedEventArgs e)
{
FrameworkElement control = sender as FrameworkElement;
if (control == null)
return;
SelectableItem item = control.DataContext as SelectableItem;
if (item == null)
return;
string yourValue = item.SubjectId;
}
I strongly recommend you read about MVVM and bindings.
What about using the Checked and UnChecked events of your CheckBox, so that you can retrieve the value of subject_id which is binded to your Label.

WPF: weird problem in dataBinding with TabControl

I'm trying to use DataBinding for dynamically populating a TabControl but have a problem. dataBinding runs fine but I would like the content of each TabItem to be independent one from the other. Here is my XAML code:
<TabControl
DockPanel.Dock="Left"
ItemsSource="{Binding OpenChats}"
Name="tabChats"
VerticalAlignment="Top"
Width="571">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBox />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
TabItems are created with different headers (as I want) but when the user types something in the TextBox inside the ContentTemplate, the same text is maintained in different tabItems and I don't want this.
What am I doing wrong?
I had same problem. This answer helped me. My solution was to remove focus from textbox when tab changed. When focus from textbox is removed, new content is set to binded property as expected.
private void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
DependencyObject focusedElement = (FocusManager.GetFocusedElement(tabControl) as DependencyObject);
if (focusedElement != null)
{
DependencyObject ancestor = VisualTreeHelper.GetParent(focusedElement);
while (ancestor != null)
{
var element = ancestor as UIElement;
if (element != null && element.Focusable)
{
element.Focus();
break;
}
ancestor = VisualTreeHelper.GetParent(ancestor);
}
}
}
or use
Text="{Binding UpdateSourceTrigger=PropertyChanged}"
on textbox binding.
The TextBox in the ContentTemplate has no Binding. Try
<TabControl.ContentTemplate>
<DataTemplate>
<TextBox Text="{Binding}" />
</DataTemplate>
</TabControl.ContentTemplate>
Adjust the bindingpath if necessary

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?

WPF ComboBox - Showing something different when selecting a value

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

Resources