Wpf TabControl create only one view at all tabs - wpf

TabControl's ItemsSource property binded to collection in the ViewModel. ContentTemplate is ListView - UserControl. All the tabs use only one ListView control (the constructor of ListView is called only once). The problem is that all tabs have a common visual state - for example, if you change the size of any item in the one tab, this change will be on all tabs. How to create a separate ListView for each tab, but at the same time use ItemsSource property?
<TabControl Grid.Row="1" Grid.Column="2" TabStripPlacement="Bottom" >
<TabControl.ContentTemplate>
<DataTemplate DataType="viewModel:ListViewModel" >
<view:ListView />
</DataTemplate>
</TabControl.ContentTemplate>
<TabControl.ItemsSource>
<Binding Path="Lists"/>
</TabControl.ItemsSource>
</TabControl>

There's no easy way of doing this.
The problem is you have a WPF Template, which is meant to be the same regardless of what data you put behind it. So one copy of the template is created, and anytime WPF encounters a ListViewModel in your UI tree it draws it using that template. Properties of that control which are not bound to the DataContext will retain their state between changing DataSources.
You could use x:Shared="False" (example here), however this creates a new copy of your template anytime WPF requests it, which includes when you switch tabs.
When [x:Shared is] set to false, modifies Windows Presentation Foundation (WPF) resource retrieval behavior such that requests for a resource will create a new instance for each request, rather than sharing the same instance for all requests.
What you really need is for the TabControl.Items to each generate a new copy of your control for each item, but that doesn't happen when you use the ItemsSource property (this is by design).
One possible alternative which might work would be to create a custom DependencyProperty that binds to your collection of items, and generates the TabItem and UserControl objects for each item in the collection. This custom DP would also need to handle the collection change events to make sure the TabItems stay in sync with your collection.
Here's one I was playing around with. It was working for simple cases, such as binding to an ObservableCollection, and adding/removing items.
public class TabControlHelpers
{
// Custom DependencyProperty for a CachedItemsSource
public static readonly DependencyProperty CachedItemsSourceProperty =
DependencyProperty.RegisterAttached("CachedItemsSource", typeof(IList), typeof(TabControlHelpers), new PropertyMetadata(null, CachedItemsSource_Changed));
// Get
public static IList GetCachedItemsSource(DependencyObject obj)
{
if (obj == null)
return null;
return obj.GetValue(CachedItemsSourceProperty) as IList;
}
// Set
public static void SetCachedItemsSource(DependencyObject obj, IEnumerable value)
{
if (obj != null)
obj.SetValue(CachedItemsSourceProperty, value);
}
// Change Event
public static void CachedItemsSource_Changed(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (!(obj is TabControl))
return;
var changeAction = new NotifyCollectionChangedEventHandler(
(o, args) =>
{
var tabControl = obj as TabControl;
if (tabControl != null)
UpdateTabItems(tabControl);
});
// if the bound property is an ObservableCollection, attach change events
INotifyCollectionChanged newValue = e.NewValue as INotifyCollectionChanged;
INotifyCollectionChanged oldValue = e.OldValue as INotifyCollectionChanged;
if (oldValue != null)
newValue.CollectionChanged -= changeAction;
if (newValue != null)
newValue.CollectionChanged += changeAction;
UpdateTabItems(obj as TabControl);
}
static void UpdateTabItems(TabControl tc)
{
if (tc == null)
return;
IList itemsSource = GetCachedItemsSource(tc);
if (itemsSource == null || itemsSource.Count == null)
{
if (tc.Items.Count > 0)
tc.Items.Clear();
return;
}
// loop through items source and make sure datacontext is correct for each one
for(int i = 0; i < itemsSource.Count; i++)
{
if (tc.Items.Count <= i)
{
TabItem t = new TabItem();
t.DataContext = itemsSource[i];
t.Content = new UserControl1(); // Should be Dynamic...
tc.Items.Add(t);
continue;
}
TabItem current = tc.Items[i] as TabItem;
if (current == null)
continue;
if (current.DataContext == itemsSource[i])
continue;
current.DataContext = itemsSource[i];
}
// loop backwards and cleanup extra tabs
for (int i = tc.Items.Count; i > itemsSource.Count; i--)
{
tc.Items.RemoveAt(i - 1);
}
}
}
Its used from the XAML like this :
<TabControl local:TabControlHelpers.CachedItemsSource="{Binding Values}">
<TabControl.Resources>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Header" Value="{Binding SomeString}" />
</Style>
</TabControl.Resources>
</TabControl>
A few things to note :
TabItem.Header is not set, so you'll have to setup a binding for it in TabControl.Resources
DependencyProperty implementation currently hardcodes the creation of the new UserControl. May want to do that some other way, such as trying to use a template property or perhaps a different DP to tell it what UserControl to create
Would probably need more testing... not sure if there's any issues with memory leaks due to change handler, etc

Based on #Rachel answer I made a few modifications.
First of all, you now have to specify a user control type as content template which is dynamically created.
I have also corrected a mistake in collectionChanged handler removal.
The code is the following:
public static class TabControlExtension
{
// Custom DependencyProperty for a CachedItemsSource
public static readonly DependencyProperty CachedItemsSourceProperty =
DependencyProperty.RegisterAttached("CachedItemsSource", typeof(IList), typeof(TabControlExtension), new PropertyMetadata(null, CachedItemsSource_Changed));
// Custom DependencyProperty for a ItemsContentTemplate
public static readonly DependencyProperty ItemsContentTemplateProperty =
DependencyProperty.RegisterAttached("ItemsContentTemplate", typeof(Type), typeof(TabControlExtension), new PropertyMetadata(null, CachedItemsSource_Changed));
// Get items
public static IList GetCachedItemsSource(DependencyObject dependencyObject)
{
if (dependencyObject == null)
return null;
return dependencyObject.GetValue(CachedItemsSourceProperty) as IList;
}
// Set items
public static void SetCachedItemsSource(DependencyObject dependencyObject, IEnumerable value)
{
if (dependencyObject != null)
dependencyObject.SetValue(CachedItemsSourceProperty, value);
}
// Get ItemsContentTemplate
public static Type GetItemsContentTemplate(DependencyObject dependencyObject)
{
if (dependencyObject == null)
return null;
return dependencyObject.GetValue(ItemsContentTemplateProperty) as Type;
}
// Set ItemsContentTemplate
public static void SetItemsContentTemplate(DependencyObject dependencyObject, IEnumerable value)
{
if (dependencyObject != null)
dependencyObject.SetValue(ItemsContentTemplateProperty, value);
}
// Change Event
public static void CachedItemsSource_Changed(
DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
if (!(dependencyObject is TabControl))
return;
var changeAction = new NotifyCollectionChangedEventHandler(
(o, args) =>
{
if (dependencyObject is TabControl tabControl && GetItemsContentTemplate(tabControl) != null && GetCachedItemsSource(tabControl) != null)
UpdateTabItems(tabControl);
});
// if the bound property is an ObservableCollection, attach change events
if (e.OldValue is INotifyCollectionChanged oldValue)
oldValue.CollectionChanged -= changeAction;
if (e.NewValue is INotifyCollectionChanged newValue)
newValue.CollectionChanged += changeAction;
if (GetItemsContentTemplate(dependencyObject) != null && GetCachedItemsSource(dependencyObject) != null)
UpdateTabItems(dependencyObject as TabControl);
}
private static void UpdateTabItems(TabControl tabControl)
{
if (tabControl == null)
return;
IList itemsSource = GetCachedItemsSource(tabControl);
if (itemsSource == null || itemsSource.Count == 0)
{
if (tabControl.Items.Count > 0)
tabControl.Items.Clear();
return;
}
// loop through items source and make sure datacontext is correct for each one
for (int i = 0; i < itemsSource.Count; i++)
{
if (tabControl.Items.Count <= i)
{
TabItem tabItem = new TabItem
{
DataContext = itemsSource[i],
Content = Activator.CreateInstance(GetItemsContentTemplate(tabControl))
};
tabControl.Items.Add(tabItem);
continue;
}
TabItem current = tabControl.Items[i] as TabItem;
if (!(tabControl.Items[i] is TabItem))
continue;
if (current.DataContext == itemsSource[i])
continue;
current.DataContext = itemsSource[i];
}
// loop backwards and cleanup extra tabs
for (int i = tabControl.Items.Count; i > itemsSource.Count; i--)
{
tabControl.Items.RemoveAt(i - 1);
}
}
}
This one is used the following way:
<TabControl main:TabControlExtension.CachedItemsSource="{Binding Channels}" main:TabControlExtension.ItemsContentTemplate="{x:Type YOURUSERCONTROLTYPE}">
<TabControl.Resources>
<Style BasedOn="{StaticResource {x:Type TabItem}}" TargetType="{x:Type TabItem}">
<Setter Property="Header" Value="{Binding Name}" />
</Style>
</TabControl.Resources>
</TabControl>

Related

Add trigger to every UserControl

My application has about 15 different UserControls that are uniformly loaded into a content-area at runtime.
My project is 100% MVVM-compliant, so I have the following XAML inserted into every UserControl's XAML:
<UserControl
...
xmlns:intr="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
>
<intr:Interaction.Triggers>
<intr:EventTrigger EventName="Loaded">
<intr:InvokeCommandAction Command="{Binding ViewLoadedCommand}"/>
</intr:EventTrigger>
</intr:Interaction.Triggers>
<!-- Rest of UserControl content here -->
</UserControl>
Obviously this isn't ideal and is also a violation of DRY.
What is the best way to have this XAML applied to this set of UserControls? (but not every UserControl, so a simple <Style Type="UserControl" /> would be inappropriate).
I use behavior implemented as attached property. It has two major advantages over System.Windows.Interactivity:
it can be defined in style.
much less xaml code in the views
in your case, the view could look like:
<UserControl ...
my:AttachedCommands.LoadedCommand="{Binding ViewLoadedCommand}">
In my solution, I'm not using commands, but I call methods on viewmodel if the viewmodel implements IViewModelLifeCycle interface:
public interface IViewModelLifeCycle
{
void Activate(object extraData);
void Deactivate();
}
All my views uses this style:
<Style x:Key="ViewBaseStyle">
<Setter Property="my:ViewModelLifeCycleBehavior.ActivateOnLoad" Value="True" />
and the behavior:
public static class ViewModelLifeCycleBehavior
{
public static readonly DependencyProperty ActivateOnLoadProperty = DependencyProperty.RegisterAttached("ActivateOnLoad", typeof (bool), typeof (ViewModelLifeCycleBehavior),
new PropertyMetadata(ActivateOnLoadPropertyChanged));
public static void SetActivateOnLoad(FrameworkElement element, bool value)
{
element.SetValue(ActivateOnLoadProperty, value);
}
public static bool GetActivateOnLoad(FrameworkElement element)
{
return (bool)element.GetValue(ActivateOnLoadProperty);
}
private static void ActivateOnLoadPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (DesignerProperties.GetIsInDesignMode(obj)) return;
var element = (FrameworkElement)obj;
element.Loaded -= ElementLoaded;
element.Unloaded -= ElementUnloaded;
if ((bool) args.NewValue == true)
{
element.Loaded += ElementLoaded;
element.Unloaded += ElementUnloaded;
}
}
static void ElementLoaded(object sender, RoutedEventArgs e)
{
var element = (FrameworkElement) sender;
var viewModel = (IViewModelLifeCycle) element.DataContext;
if (viewModel == null)
{
DependencyPropertyChangedEventHandler dataContextChanged = null;
dataContextChanged = (o, _e) =>
{
ElementLoaded(sender, e);
element.DataContextChanged -= dataContextChanged;
};
element.DataContextChanged += dataContextChanged;
}
else if (element.ActualHeight > 0 && element.ActualWidth > 0) //to avoid activating twice since loaded event is called twice on TabItems' subtrees
{
viewModel.Activate(null);
}
}
private static void ElementUnloaded(object sender, RoutedEventArgs e)
{
var element = (FrameworkElement)sender;
var viewModel = (IViewModelLifeCycle)element.DataContext;
viewModel.Deactivate();
}
}
TIP:
Create your custom Item Template in Visual Studio for View and ViewModel. its very easy and saves a lot of time. The item template can contain xaml code with the trigger/behaviour, pointing to your base style, your d:DataContext definition and your viewmodel class.
you could create a "base-load" Usercontrol with the Interaction.Triggers and just put a ContentPresenter in it where you bind the real content to.
<UserControl x:class="OneOfMyOtherControls">
<MyBaseUserControl>
<!-- your UserControl Content-->
</MyBaseUserControl>
</UserControl>

Get the ComboBox that a ComboBoxItem resides in

I need to find the ComboBox that a ComboBoxItem resides in.
In codebehind I catch an event when a ComboBoxItem is clicked, but I don't know which one of several ComboBoxes that the specific ComboBoxItem belongs to. How do I find the ComboBox?
Normally you can use LogicalTreeHelper.GetParent() and traverse up the logical tree from the ComboBoxItem to find the ComboBox. But this only works if the ComboBoxItems are added to the ComboBox manually, not when the items are applied to the ComboBox with databinding. When using databinding, the ComboBoxItems do not have the ComboBox as a logical parent (I don't understand why).
Any ideas?
More info:
Below is some code reconstructing my problem (not my actual code). If I would change from databinding the ComboBoxItems to setting them manually (in the XAML), the variable "comboBox" would be set to the correct ComboBox. Now comboBox is only null.
XAML:
<ComboBox Name="MyComboBox" ItemsSource="{Binding Path=ComboBoxItems, Mode=OneTime}" />
CodeBehind:
public MainWindow()
{
InitializeComponent();
MyComboBox.DataContext = this;
this.PreviewMouseDown += MainWindow_MouseDown;
}
public BindingList<string> ComboBoxItems
{
get
{
BindingList<string> items = new BindingList<string>();
items.Add("Item E");
items.Add("Item F");
items.Add("Item G");
items.Add("Item H");
return items;
}
}
private void MainWindow_MouseDown(object sender, MouseButtonEventArgs e)
{
DependencyObject clickedObject = e.OriginalSource as DependencyObject;
ComboBoxItem comboBoxItem = FindVisualParent<ComboBoxItem>(clickedObject);
if (comboBoxItem != null)
{
ComboBox comboBox = FindLogicalParent<ComboBox>(comboBoxItem);
}
}
//Tries to find visual parent of the specified type.
private static T FindVisualParent<T>(DependencyObject childElement) where T : DependencyObject
{
DependencyObject parent = VisualTreeHelper.GetParent(childElement);
T parentAsT = parent as T;
if (parent == null)
{
return null;
}
else if (parentAsT != null)
{
return parentAsT;
}
return FindVisualParent<T>(parent);
}
//Tries to find logical parent of the specified type.
private static T FindLogicalParent<T>(DependencyObject childElement) where T : DependencyObject
{
DependencyObject parent = LogicalTreeHelper.GetParent(childElement);
T parentAsT = parent as T;
if (parent == null)
{
return null;
}
else if(parentAsT != null)
{
return parentAsT;
}
return FindLogicalParent<T>(parent);
}
This is probably what you are looking for:
var comboBox = ItemsControl.ItemsControlFromItemContainer(comboBoxItem) as ComboBox;
I love how descriptive that method-name is.
On a side-note, there are some other useful methods which can be found in the property ItemsControl.ItemContainerGenerator which let you get the container associated with the templated data and vice versa.
On another side-note, you usually should not be using any of them and instead use data-binding actually.

What is the proper way to handle multiple datagrids in a tab control so that cells leave edit mode when the tabs are changed?

In wpf I setup a tab control that binds to a collection of objects each object has a data template with a data grid presenting the data. If I select a particular cell and put it into edit mode, leaving the grid by going to another tab this will cause the exception below to be thrown on returning the datagrid:
'DeferRefresh' is not allowed during an AddNew or EditItem transaction.
It appears that the cell never left edit mode. Is there an easy way to take the cell out of edit mode, or is something else going on here?
Update: It looks like if I do not bind the tab control to the data source, but instead explicitly define each tab and then bind each item in the data source to a content control this problem goes away. This is not really a great solution, so I would still like to know how to bind the collection directly to the tab control.
Update: So what I have actually done for my own solution is to use a ListView and a content control in place of a tab control. I use a style to make the list view look tab like. The view model exposes a set of child view models and allows the user to select one via the list view. The content control then presents the selected view model and each view model has an associated data template which contains the data grid. With this setup switching between view models while in edit mode on the grid will properly end edit mode and save the data.
Here is the xaml for setting this up:
<ListView ItemTemplate="{StaticResource MakeItemsLookLikeTabs}"
ItemsSource="{Binding ViewModels}"
SelectedItem="{Binding Selected}"
Style="{StaticResource MakeItLookLikeATabControl}"/>
<ContentControl Content="{Binding Selected}">
I'll accept Phil's answer as that should work also, but for me the solution above seems like it will be more portable between projects.
I implemented a behavior for the DataGrid based on code I found in this thread.
Usage:<DataGrid local:DataGridCommitEditBehavior.CommitOnLostFocus="True" />
Code:
using System.Collections.Generic;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
/// <summary>
/// Provides an ugly hack to prevent a bug in the data grid.
/// https://connect.microsoft.com/VisualStudio/feedback/details/532494/wpf-datagrid-and-tabcontrol-deferrefresh-exception
/// </summary>
public class DataGridCommitEditBehavior
{
public static readonly DependencyProperty CommitOnLostFocusProperty =
DependencyProperty.RegisterAttached(
"CommitOnLostFocus",
typeof(bool),
typeof(DataGridCommitEditBehavior),
new UIPropertyMetadata(false, OnCommitOnLostFocusChanged));
/// <summary>
/// A hack to find the data grid in the event handler of the tab control.
/// </summary>
private static readonly Dictionary<TabPanel, DataGrid> ControlMap = new Dictionary<TabPanel, DataGrid>();
public static bool GetCommitOnLostFocus(DataGrid datagrid)
{
return (bool)datagrid.GetValue(CommitOnLostFocusProperty);
}
public static void SetCommitOnLostFocus(DataGrid datagrid, bool value)
{
datagrid.SetValue(CommitOnLostFocusProperty, value);
}
private static void CommitEdit(DataGrid dataGrid)
{
dataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
dataGrid.CommitEdit(DataGridEditingUnit.Row, true);
}
private static DataGrid GetParentDatagrid(UIElement element)
{
UIElement childElement; // element from which to start the tree navigation, looking for a Datagrid parent
if (element is ComboBoxItem)
{
// Since ComboBoxItem.Parent is null, we must pass through ItemsPresenter in order to get the parent ComboBox
var parentItemsPresenter = VisualTreeFinder.FindParentControl<ItemsPresenter>(element as ComboBoxItem);
var combobox = parentItemsPresenter.TemplatedParent as ComboBox;
childElement = combobox;
}
else
{
childElement = element;
}
var parentDatagrid = VisualTreeFinder.FindParentControl<DataGrid>(childElement);
return parentDatagrid;
}
private static TabPanel GetTabPanel(TabControl tabControl)
{
return
(TabPanel)
tabControl.GetType().InvokeMember(
"ItemsHost",
BindingFlags.NonPublic | BindingFlags.GetProperty | BindingFlags.Instance,
null,
tabControl,
null);
}
private static void OnCommitOnLostFocusChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
var dataGrid = depObj as DataGrid;
if (dataGrid == null)
{
return;
}
if (e.NewValue is bool == false)
{
return;
}
var parentTabControl = VisualTreeFinder.FindParentControl<TabControl>(dataGrid);
var tabPanel = GetTabPanel(parentTabControl);
if (tabPanel != null)
{
ControlMap[tabPanel] = dataGrid;
}
if ((bool)e.NewValue)
{
// Attach event handlers
if (parentTabControl != null)
{
tabPanel.PreviewMouseLeftButtonDown += OnParentTabControlPreviewMouseLeftButtonDown;
}
dataGrid.LostKeyboardFocus += OnDataGridLostFocus;
dataGrid.DataContextChanged += OnDataGridDataContextChanged;
dataGrid.IsVisibleChanged += OnDataGridIsVisibleChanged;
}
else
{
// Detach event handlers
if (parentTabControl != null)
{
tabPanel.PreviewMouseLeftButtonDown -= OnParentTabControlPreviewMouseLeftButtonDown;
}
dataGrid.LostKeyboardFocus -= OnDataGridLostFocus;
dataGrid.DataContextChanged -= OnDataGridDataContextChanged;
dataGrid.IsVisibleChanged -= OnDataGridIsVisibleChanged;
}
}
private static void OnDataGridDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var dataGrid = (DataGrid)sender;
CommitEdit(dataGrid);
}
private static void OnDataGridIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var senderDatagrid = (DataGrid)sender;
if ((bool)e.NewValue == false)
{
CommitEdit(senderDatagrid);
}
}
private static void OnDataGridLostFocus(object sender, KeyboardFocusChangedEventArgs e)
{
var dataGrid = (DataGrid)sender;
var focusedElement = Keyboard.FocusedElement as UIElement;
if (focusedElement == null)
{
return;
}
var focusedDatagrid = GetParentDatagrid(focusedElement);
// Let's see if the new focused element is inside a datagrid
if (focusedDatagrid == dataGrid)
{
// If the new focused element is inside the same datagrid, then we don't need to do anything;
// this happens, for instance, when we enter in edit-mode: the DataGrid element loses keyboard-focus,
// which passes to the selected DataGridCell child
return;
}
CommitEdit(dataGrid);
}
private static void OnParentTabControlPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var dataGrid = ControlMap[(TabPanel)sender];
CommitEdit(dataGrid);
}
}
public static class VisualTreeFinder
{
/// <summary>
/// Find a specific parent object type in the visual tree
/// </summary>
public static T FindParentControl<T>(DependencyObject outerDepObj) where T : DependencyObject
{
var dObj = VisualTreeHelper.GetParent(outerDepObj);
if (dObj == null)
{
return null;
}
if (dObj is T)
{
return dObj as T;
}
while ((dObj = VisualTreeHelper.GetParent(dObj)) != null)
{
if (dObj is T)
{
return dObj as T;
}
}
return null;
}
}
I have managed to work around this issue by detecting when the user clicks on a TabItem and then committing edits on visible DataGrid in the TabControl. I'm assuming the user will expect their changes to still be there when they click back.
Code snippet:
// PreviewMouseDown event handler on the TabControl
private void TabControl_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (IsUnderTabHeader(e.OriginalSource as DependencyObject))
CommitTables(yourTabControl);
}
private bool IsUnderTabHeader(DependencyObject control)
{
if (control is TabItem)
return true;
DependencyObject parent = VisualTreeHelper.GetParent(control);
if (parent == null)
return false;
return IsUnderTabHeader(parent);
}
private void CommitTables(DependencyObject control)
{
if (control is DataGrid)
{
DataGrid grid = control as DataGrid;
grid.CommitEdit(DataGridEditingUnit.Row, true);
return;
}
int childrenCount = VisualTreeHelper.GetChildrenCount(control);
for (int childIndex = 0; childIndex < childrenCount; childIndex++)
CommitTables(VisualTreeHelper.GetChild(control, childIndex));
}
This is in the code behind.
This bug is solved in the .NET Framework 4.5. You can download it at this link.
What I think you should do is pretty close to what #myermian said.
There is an event called CellEditEnding end this event would allow you to intercept and make the decision to drop the unwanted row.
private void dataGrid1_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
DataGrid grid = (DataGrid)sender;
TextBox cell = (TextBox)e.EditingElement;
if(String.IsNullOrEmpty(cell.Text) && e.EditAction == DataGridEditAction.Commit)
{
grid.CancelEdit(DataGridEditingUnit.Row);
e.Cancel = true;
}
}

WPF DataGrid: Binding DataGridColumn visibility to ContextMenu MenuItems IsChecked (MVVM)

I want to control DataGrid column visibility through a ContextMenu available to the user by right-clicking the column header. The ContextMenu displays the names of all available columns. I am using MVVM design pattern.
My question is: How do I bind the DataGridColumn's Visibility property to the IsChecked property of a MenuItem located in the ContextMenu.
Some mockup code:
<UserControl.Resources>
<ContextMenu x:Key="ColumnHeaderContextMenu">
<MenuItem Header="Menu Item..1" IsCheckable="True" />
</ContextMenu>
<Style x:Key="ColumnHeaderStyle"
TargetType="{x:Type toolkit:DataGridColumnHeader}">
<Setter Property="ContextMenu"
Value="{StaticResource ColumnHeaderContextMenu}" />
</Style>
<BooleanToVisibilityConverter x:Key="booleanToVisibilityConverter" />
</UserControl.Resources>
...flaf flaf flaf
<toolkit:DataGrid x:Name="MyGrid" AutoGenerateColumns="False"
ItemsSource="{Binding MyCollection, Mode=Default}"
EnableColumnVirtualization="True" IsReadOnly="True"
ColumnHeaderStyle="{StaticResource ColumnHeaderStyle}">
<toolkit:DataGrid.Columns>
<toolkit:DataGridTextColumn Binding="{Binding Path=MyEntry}"
Header="MyEntry" Visibility="{Binding IsChecked, Converter=
{StaticResource booleanToVisibilityConverter}.... />
</toolkit:DataGrid.Columns>
</toolkit:DataGrid>
If I am being unclear please let me know and I will attempt to elaborate.
Cheers,
Updated 2021-09-29: Pasted the code from my old blog. I haven't worked with WPF in many years I know nothing about it anymore. This site has limits on post length so I had to share some of the example code using CodePile. See the links at bottom for usage. I believe the idea was the sample I made works in many scenarios versus just auto generated columns. I was working on a project where the columns were not known until runtime and could change dynamically. Also note there was a png file for the "eye" graphic, you'll need your own probably.
DataGridAPs.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Markup;
using System.Windows.Media;
using Microsoft.Windows.Controls;
using Microsoft.Windows.Controls.Primitives;
namespace CanUserHideColumnDemo
{
public static class DataGridAPs
{
#region HideColumns
#region HideColumnsHeader
public static readonly DependencyProperty HideColumnsHeaderProperty =
DependencyProperty.RegisterAttached("HideColumnsHeader",
typeof(object), typeof(DataGridAPs));
public static object GetHideColumnsHeader(DataGrid obj)
{
return obj.GetValue(HideColumnsHeaderProperty);
}
public static void SetHideColumnsHeader(DataGrid obj, object value)
{
obj.SetValue(HideColumnsHeaderProperty, value);
}
#endregion HideColumnsHeader
#region HideColumnsHeaderTemplate
public static readonly DependencyProperty HideColumnsHeaderTemplateProperty =
DependencyProperty.RegisterAttached("HideColumnsHeaderTemplate",
typeof(DataTemplate), typeof(DataGridAPs));
public static DataTemplate GetHideColumnsHeaderTemplate(DataGrid obj)
{
return (DataTemplate)obj.GetValue(HideColumnsHeaderTemplateProperty);
}
public static void SetHideColumnsHeaderTemplate(DataGrid obj, DataTemplate value)
{
obj.SetValue(HideColumnsHeaderTemplateProperty, value);
}
#endregion HideColumnsHeaderTemplate
#region HideColumnsIcon
public static readonly DependencyProperty HideColumnsIconProperty =
DependencyProperty.RegisterAttached("HideColumnsIcon",
typeof(object), typeof(DataGridAPs));
public static object GetHideColumnsIcon(DataGrid obj)
{
return obj.GetValue(HideColumnsIconProperty);
}
public static void SetHideColumnsIcon(DataGrid obj, object value)
{
obj.SetValue(HideColumnsIconProperty, value);
}
#endregion HideColumnsIcon
#region CanUserHideColumns
public static readonly DependencyProperty CanUserHideColumnsProperty =
DependencyProperty.RegisterAttached("CanUserHideColumns",
typeof(bool), typeof(DataGridAPs),
new UIPropertyMetadata(false, OnCanUserHideColumnsChanged));
public static bool GetCanUserHideColumns(DataGrid obj)
{
return (bool)obj.GetValue(CanUserHideColumnsProperty);
}
public static void SetCanUserHideColumns(DataGrid obj, bool value)
{
obj.SetValue(CanUserHideColumnsProperty, value);
}
private static void OnCanUserHideColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = d as DataGrid;
if (dataGrid == null)
return;
if ((bool)e.NewValue == false)
{
dataGrid.Loaded -= new RoutedEventHandler(dataGrid_Loaded);
RemoveAllItems(dataGrid);
return;
}
if (!dataGrid.IsLoaded)
{
dataGrid.Loaded -= new RoutedEventHandler(dataGrid_Loaded);
dataGrid.Loaded += new RoutedEventHandler(dataGrid_Loaded);
}
else
SetupColumnHeaders(dataGrid);
}
private static void dataGrid_Loaded(object sender, RoutedEventArgs e)
{
DataGrid dataGrid = sender as DataGrid;
if (dataGrid == null)
return;
if (BindingOperations.IsDataBound(dataGrid, DataGrid.ItemsSourceProperty))
{
Binding b = BindingOperations.GetBinding(dataGrid, DataGrid.ItemsSourceProperty);
dataGrid.TargetUpdated += new EventHandler<DataTransferEventArgs>(dataGrid_TargetUpdated);
string xaml = XamlWriter.Save(b);
Binding b2 = XamlReader.Parse(xaml) as Binding;
if (b2 != null)
{
b2.NotifyOnTargetUpdated = true;
BindingOperations.ClearBinding(dataGrid, DataGrid.ItemsSourceProperty);
BindingOperations.SetBinding(dataGrid, DataGrid.ItemsSourceProperty, b2);
}
}
else
SetupColumnHeaders(dataGrid);
}
private static void dataGrid_TargetUpdated(object sender, DataTransferEventArgs e)
{
if (e.Property != DataGrid.ItemsSourceProperty)
return;
DataGrid dataGrid = sender as DataGrid;
if (dataGrid == null)
return;
EventHandler handler = null;
handler = delegate
{
RemoveAllItems(dataGrid);
if (SetupColumnHeaders(dataGrid))
dataGrid.LayoutUpdated -= handler;
};
dataGrid.LayoutUpdated += handler;
}
private static DataGridColumnHeader[] GetColumnHeaders(DataGrid dataGrid)
{
if (dataGrid == null)
return null;
dataGrid.UpdateLayout();
DataGridColumnHeader[] columnHeaders = CustomVisualTreeHelper<DataGridColumnHeader>.FindChildrenRecursive(dataGrid);
return (from DataGridColumnHeader columnHeader in columnHeaders
where columnHeader != null && columnHeader.Column != null
select columnHeader).ToArray();
}
private static string GetColumnName(DataGridColumn column)
{
if (column == null)
return string.Empty;
if (column.Header != null)
return column.Header.ToString();
else
return string.Format("Column {0}", column.DisplayIndex);
}
private static MenuItem GenerateItem(DataGrid dataGrid, DataGridColumn column)
{
if (column == null)
return null;
MenuItem item = new MenuItem();
item.Tag = column;
item.Header = GetColumnName(column);
if (string.IsNullOrEmpty(item.Header as string))
return null;
item.ToolTip = string.Format("Toggle column '{0}' visibility.", item.Header);
item.IsCheckable = true;
item.IsChecked = column.Visibility == Visibility.Visible;
item.Checked += delegate
{
SetItemIsChecked(dataGrid, column, true);
};
item.Unchecked += delegate
{
SetItemIsChecked(dataGrid, column, false);
};
return item;
}
public static MenuItem[] GetAttachedItems(DataGridColumnHeader columnHeader)
{
if (columnHeader == null || columnHeader.ContextMenu == null)
return null;
ItemsControl itemsContainer = (from object i in columnHeader.ContextMenu.Items
where i is MenuItem && ((MenuItem)i).Tag != null && ((MenuItem)i).Tag.ToString() == "ItemsContainer"
select i).FirstOrDefault() as MenuItem;
if (itemsContainer == null)
itemsContainer = columnHeader.ContextMenu;
return (from object i in itemsContainer.Items
where i is MenuItem && ((MenuItem)i).Tag is DataGridColumn
select i).Cast<MenuItem>().ToArray();
}
private static DataGridColumn GetColumnFromName(DataGrid dataGrid, string columnName)
{
if (string.IsNullOrEmpty(columnName))
return null;
foreach (DataGridColumn column in dataGrid.Columns)
{
if (GetColumnName(column) == columnName)
return column;
}
return null;
}
private static DataGridColumnHeader GetColumnHeaderFromColumn(DataGrid dataGrid, DataGridColumn column)
{
if (dataGrid == null || column == null)
return null;
DataGridColumnHeader[] columnHeaders = GetColumnHeaders(dataGrid);
return (from DataGridColumnHeader columnHeader in columnHeaders
where columnHeader.Column == column
select columnHeader).FirstOrDefault();
}
public static void RemoveAllItems(DataGrid dataGrid)
{
if (dataGrid == null)
return;
foreach (DataGridColumn column in dataGrid.Columns)
{
RemoveAllItems(dataGrid, column);
}
}
public static void RemoveAllItems(DataGrid dataGrid, DataGridColumn column)
{
if (dataGrid == null || column == null)
return;
DataGridColumnHeader columnHeader = GetColumnHeaderFromColumn(dataGrid, column);
List<MenuItem> itemsToRemove = new List<MenuItem>();
if (columnHeader == null)
return;
// Mark items and/or items container for removal.
if (columnHeader.ContextMenu != null)
{
foreach (object item in columnHeader.ContextMenu.Items)
{
if (item is MenuItem && ((MenuItem)item).Tag != null
&& (((MenuItem)item).Tag.ToString() == "ItemsContainer" || ((MenuItem)item).Tag is DataGridColumn))
itemsToRemove.Add((MenuItem)item);
}
}
// Remove items and/or items container.
foreach (MenuItem item in itemsToRemove)
{
columnHeader.ContextMenu.Items.Remove(item);
}
}
public static void ResetupColumnHeaders(DataGrid dataGrid)
{
RemoveAllItems(dataGrid);
SetupColumnHeaders(dataGrid);
}
private static void SetItemIsChecked(DataGrid dataGrid, DataGridColumn column, bool isChecked)
{
if (dataGrid == null || column == null)
return;
// Deny request if there are no other columns visible. Otherwise,
// they'd have no way of changing the visibility of any columns
// again.
//if (!isChecked && (from DataGridColumn c in dataGrid.Columns
// where c.Visibility == Visibility.Visible
// select c).Count() < 2)
// return;
if (isChecked && column.Visibility != Visibility.Visible)
{
ShowColumn(dataGrid, column);
}
else if (!isChecked)
column.Visibility = Visibility.Hidden;
DataGridColumnHeader[] columnHeaders = GetColumnHeaders(dataGrid);
ItemsControl itemsContainer = null;
object containerHeader = GetHideColumnsHeader(dataGrid);
foreach (DataGridColumnHeader columnHeader in columnHeaders)
{
itemsContainer = null;
if (columnHeader != null)
{
if (columnHeader.ContextMenu == null)
continue;
itemsContainer = (from object i in columnHeader.ContextMenu.Items
where i is MenuItem && ((MenuItem)i).Header == containerHeader
select i).FirstOrDefault() as MenuItem;
}
if (itemsContainer == null)
itemsContainer = columnHeader.ContextMenu;
foreach (object item in itemsContainer.Items)
{
if (item is MenuItem && ((MenuItem)item).Tag != null && ((MenuItem)item).Tag is DataGridColumn
&& ((MenuItem)item).Header.ToString() == GetColumnName(column))
{
((MenuItem)item).IsChecked = isChecked;
}
}
}
}
private static void SetupColumnHeader(DataGridColumnHeader columnHeader)
{
if (columnHeader == null)
return;
DataGrid dataGrid = CustomVisualTreeHelper<DataGrid>.FindAncestor(columnHeader);
if (dataGrid == null)
return;
DataGridColumnHeader[] columnHeaders = GetColumnHeaders(dataGrid);
if (columnHeaders == null)
return;
SetupColumnHeader(dataGrid, columnHeaders, columnHeader);
}
private static void SetupColumnHeader(DataGrid dataGrid, DataGridColumnHeader[] columnHeaders, DataGridColumnHeader columnHeader)
{
if (columnHeader.ContextMenu == null)
columnHeader.ContextMenu = new ContextMenu();
ItemsControl itemsContainer = null;
itemsContainer = columnHeader.ContextMenu;
object containerHeader = GetHideColumnsHeader(dataGrid);
if (containerHeader != null)
{
MenuItem ic = (from object i in columnHeader.ContextMenu.Items
where i is MenuItem && ((MenuItem)i).Tag != null && ((MenuItem)i).Tag.ToString() == "ItemsContainer"
select i).FirstOrDefault() as MenuItem;
if (ic == null)
{
itemsContainer = new MenuItem()
{
Header = containerHeader,
HeaderTemplate = GetHideColumnsHeaderTemplate(dataGrid) as DataTemplate,
Icon = GetHideColumnsIcon(dataGrid),
Tag = "ItemsContainer"
};
columnHeader.ContextMenu.Items.Add(itemsContainer);
}
else
return;
}
foreach (DataGridColumnHeader columnHeader2 in columnHeaders)
{
if (columnHeader2 != columnHeader
&& itemsContainer is ContextMenu
&& columnHeader2.ContextMenu == itemsContainer)
{
continue;
}
itemsContainer.Items.Add(GenerateItem(dataGrid, columnHeader2.Column));
}
}
public static bool SetupColumnHeaders(DataGrid dataGrid)
{
DataGridColumnHeader[] columnHeaders = GetColumnHeaders(dataGrid);
if (columnHeaders == null || columnHeaders.Count() == 0)
return false;
RemoveAllItems(dataGrid);
columnHeaders = GetColumnHeaders(dataGrid);
foreach (DataGridColumnHeader columnHeader in columnHeaders)
{
SetupColumnHeader(dataGrid, columnHeaders, columnHeader);
}
return true;
}
/// <summary>
/// Shows a column within the datagrid, which is not straightforward
/// because the datagrid not only hides a column when you tell it to
/// do so, but it also completely destroys its associated column
/// header. Meaning we need to set it up again. Before we can do
/// so we have to turn all columns back on again so we can get a
/// complete list of their column headers, then turn them back off
/// again.
/// </summary>
/// <param name="dataGrid"></param>
/// <param name="column"></param>
private static void ShowColumn(DataGrid dataGrid, DataGridColumn column)
{
if (dataGrid == null || column == null)
return;
column.Visibility = Visibility.Visible;
// Turn all columns on, but store their original visibility so we
// can restore it after we're done.
Dictionary<DataGridColumn, Visibility> vis = new Dictionary<DataGridColumn, Visibility>();
foreach (DataGridColumn c in dataGrid.Columns)
{
vis.Add(c, c.Visibility);
c.Visibility = Visibility.Visible;
}
dataGrid.UpdateLayout();
DataGridColumnHeader columnHeader = GetColumnHeaderFromColumn(dataGrid, column);
SetupColumnHeader(columnHeader);
foreach (DataGridColumn c in vis.Keys)
{
if ((Visibility)vis[c] != Visibility.Visible)
{
c.Visibility = (Visibility)vis[c];
}
}
dataGrid.UpdateLayout();
// Now we need to uncheck items that are associated with hidden
// columns.
SyncItemsOnColumnHeader(columnHeader);
}
private static void SyncItemsOnColumnHeader(DataGridColumnHeader columnHeader)
{
bool isVisible;
foreach (MenuItem item in GetAttachedItems(columnHeader))
{
if (item.Tag is DataGridColumn)
{
isVisible = ((DataGridColumn)item.Tag).Visibility == Visibility.Visible ? true : false;
if (item.IsChecked != isVisible)
{
item.IsChecked = isVisible;
}
}
}
}
#endregion CanUserHideColumns
#region CustomVisualTreeHelper
private static class CustomVisualTreeHelper<TReturn> where TReturn : DependencyObject
{
public static TReturn FindAncestor(DependencyObject descendant)
{
DependencyObject parent = descendant;
while (parent != null && !(parent is TReturn))
{
parent = VisualTreeHelper.GetParent(parent);
}
if (parent != null)
{
return (TReturn)parent;
}
return default(TReturn);
}
public static TReturn FindChild(DependencyObject parent)
{
int childCount = VisualTreeHelper.GetChildrenCount(parent);
DependencyObject child = null;
for (int childIndex = 0; childIndex < childCount; childIndex++)
{
child = VisualTreeHelper.GetChild(parent, childIndex);
if (child is TReturn)
{
return (TReturn)(object)child;
}
}
return default(TReturn);
}
public static TReturn FindChildRecursive(DependencyObject parent)
{
int childCount = VisualTreeHelper.GetChildrenCount(parent);
DependencyObject child = null;
for (int childIndex = 0; childIndex < childCount; childIndex++)
{
child = VisualTreeHelper.GetChild(parent, childIndex);
if (child is TReturn)
{
return (TReturn)(object)child;
}
else
{
child = CustomVisualTreeHelper<TReturn>.FindChildRecursive(child);
if (child is TReturn)
{
return (TReturn)(object)child;
}
}
}
return default(TReturn);
}
public static TReturn[] FindChildren(DependencyObject parent)
{
int childCount = VisualTreeHelper.GetChildrenCount(parent);
DependencyObject child = null;
List<TReturn> children = new List<TReturn>(childCount);
for (int childIndex = 0; childIndex < childCount; childIndex++)
{
child = VisualTreeHelper.GetChild(parent, childIndex);
if (child is TReturn)
{
children[childIndex] = (TReturn)(object)child;
}
}
return children.ToArray();
}
public static TReturn[] FindChildrenRecursive(DependencyObject parent)
{
int childCount = VisualTreeHelper.GetChildrenCount(parent);
DependencyObject child = null;
List<TReturn> children = new List<TReturn>();
for (int childIndex = 0; childIndex < childCount; childIndex++)
{
child = VisualTreeHelper.GetChild(parent, childIndex);
if (child is TReturn)
{
children.Add((TReturn)(object)child);
}
children.AddRange(CustomVisualTreeHelper<TReturn>.FindChildrenRecursive(child));
}
return children.ToArray();
}
}
#endregion CustomVisualTreeHelper
#endregion HideColumns
}
}
Window1.xaml
Window1.xaml.cs
I've been looking for a generic, XAML (i.e., no code-behind), automatic and simple example of a column chooser context menu that binds to a WPF DataGrid column header. I've read literally hundreds of articles, but none of them seem to do exactly the right thing, or they're no generic enough. So here's what I think is the best combined solution:
First, put these in the resource dictionary. I'll leave it as an exercise to the reader to write the Visibility/Boolean converter to ensure the checkboxes check when the column is visible and vice-versa. Note that by defining x:Shared="False" for the context menu resource, it'll get instance-specific state which means that you can use this single template/resource for all your datagrids and they'll all maintain their own state.
<Converters:VisiblityToInverseBooleanConverter x:Key="VisiblityToInverseBooleanConverter"/>
<ContextMenu x:Key="ColumnChooserMenu" x:Shared="False"
DataContext="{Binding Path=PlacementTarget, RelativeSource={RelativeSource Self}}"
ItemsSource="{Binding Columns, RelativeSource={RelativeSource AncestorType={x:Type sdk:DataGrid}}}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="AutomationProperties.Name" Value="{Binding Header}"/>
<Setter Property="IsCheckable" Value="True" />
<Setter Property="IsChecked" Value="{Binding Visibility, Mode=TwoWay, Converter={StaticResource VisiblityToInverseBooleanConverter}}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
<Style x:Key="ColumnHeaderStyle" TargetType="{x:Type Primitives:DataGridColumnHeader}">
<Setter Property="ContextMenu" Value="{StaticResource ColumnChooserMenu}" />
</Style>
<ContextMenu x:Key="GridItemsContextMenu" >
<MenuItem Header="Launch Do Some other action"/>
</ContextMenu>
Then define the DataGrid as follows (where OrdersQuery is some data source exposed by the View-model):
<sdk:DataGrid ItemsSource="{Binding OrdersQuery}"
AutoGenerateColumns="True"
ColumnHeaderStyle="{StaticResource ColumnHeaderStyle}"
ContextMenu="{StaticResource GridItemsContextMenu}">
<!-- rest of datagrid stuff goes here -->
</sdk:DataGrid>
This will give you the following:
A context menu bound to the column headings which acts as a column chooser.
A context menu bound to the items in the grid (to perform actions on the items themselves - again, the binding of the actions is an exercise for the reader).
Hope this helps people who've been looking for an example like this.
I know this is a bit old. But I was looking at doing this and this post is much simpler:
http://iimaginec.wordpress.com/2011/07/25/binding-wpf-toolkit%E2%80%99s-datagridcolumn-to-a-viewmodel-datacontext-propogation-for-datagrid-columns-the-mvvm-way-to-interact-with-datagridcolumn/
All you need to do is set DataContext on the Columns and then bind Visibility to your ViewModel as per normal!
:) Simple and effective
Ok, this has been quite the exercise for a WPF n00b.
IanR thanks for the suggestion I used a similar aproach but it dosent take you all the way.
Here is what I have come up with if anyone can find a more consistent way of doing it I will appreciate any comments:
Impediments:
DataGridColumnHeader does not support a context menu. Therefore the context menu needs to be applied as a Style.
The contextmenu has its own datacontext so we have to use findancestor to link it to the ViewModels datacontext.
ATM the DataGrid control does not parse its datacontext to its Columns. This could be solved in codebehind however we are using the MVVM pattern so I decided to follow jamiers approach
Solution:
Place the following two blocks of code in Window.Resources
<Style x:Key="ColumnHeaderStyle"
TargetType="{x:Type toolkit:DataGridColumnHeader}">
<Setter Property="ContextMenu"
Value="{StaticResource ColumnHeaderContextMenu}" />
</Style>
<ContextMenu x:Key="ColumnHeaderContextMenu">
<MenuItem x:Name="MyMenuItem"
IsCheckable="True"
IsChecked="{Binding DataContext.IsHidden, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type toolkit:DataGrid}}}"/>
</ContextMenu>
The datagrid then looks something like this in XAML
<toolkit:DataGrid x:Name="MyGrid"
AutoGenerateColumns="False"
ItemsSource="{Binding SampleCollection, Mode=Default}"
EnableColumnVirtualization="True"
IsReadOnly="True"
ColumnHeaderStyle="{StaticResource ColumnHeaderStyle}">
<toolkit:DataGrid.Columns>
<toolkit:DataGridTextColumn Binding="{Binding Path=SamplingUser}"
Header="{Binding (FrameworkElement.DataContext).IsHidden, RelativeSource={x:Static RelativeSource.Self}}"
Visibility="{Binding (FrameworkElement.DataContext).IsHidden,
RelativeSource={x:Static RelativeSource.Self},
Converter={StaticResource booleanToVisibilityConverter}}"/>
So the visibility property on the DataGridColumn and the ischeked property are both databound to the IsHidden property on the viewModel.
In the ViewModel:
public bool IsHidden
{
get { return isHidden; }
set
{ if (value != isHidden)
{
isHidden = value;
OnPropertyChanged("IsHidden");
OnPropertyChanged("IsVisible");
}
}
}
The Helper class defined by Jaimer:
class DataGridSupport
{
static DataGridSupport()
{
DependencyProperty dp = FrameworkElement.DataContextProperty.AddOwner(typeof(DataGridColumn));
FrameworkElement.DataContextProperty.OverrideMetadata ( typeof(DataGrid), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, new PropertyChangedCallback(OnDataContextChanged)));
}
public static void OnDataContextChanged ( DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DataGrid grid = d as DataGrid ;
if ( grid != null )
{
foreach ( DataGridColumn col in grid.Columns )
{
col.SetValue ( FrameworkElement.DataContextProperty, e.NewValue );
}
}
}
}
Instanciated in the viewmodel (just to show done through Unity in real project)
private static DataGridSupport dc = new DataGridSupport();
Cheers,
Instead of booleanToVisibilityConverter you can use x:static
<Setter TargetName="UIElement" Property="UIElement.Visibility" Value="x:Static Visibility.Hidden" />
Statics in XAML:
http://msdn.microsoft.com/en-us/library/ms742135.aspx
I did try to ge this to bind to the ContextMenu using 'ElementName', but in the end, got it work using Properties in the VM, e.g.
bool _isHidden;
public bool IsHidden
{
get { return _isHidden; }
set
{
if (value != _isHidden)
{
_isHidden = value;
RaisePropertyChanged("IsHidden");
RaisePropertyChanged("IsVisible");
}
}
}
public Visibility IsVisible
{
get { return IsHidden ? Visibility.Hidden : Visibility.Visible; }
}
and in the XAML:
<Window.ContextMenu>
<ContextMenu>
<MenuItem Header="Hidden" IsCheckable="True" IsChecked="{Binding IsHidden}" />
</ContextMenu>
</Window.ContextMenu>
<toolkit:DataGrid x:Name="MyGrid" AutoGenerateColumns="False" ItemsSource="{Binding MyCollection, Mode=Default}" EnableColumnVirtualization="True" IsReadOnly="True" ColumnHeaderStyle="{StaticResource ColumnHeaderStyle}">
<toolkit:DataGrid.Columns>
<toolkit:DataGridTextColumn Binding="{Binding Path=MyEntry}" Header="MyEntry" Visibility="{Binding Path=IsVisible, Mode=OneWay}" />
</toolkit:DataGrid.Columns>
</toolkit:DataGrid>

Silverlight Toolkit Treeview: Getting the parent of a selected item

I'm using the TreeView component from the Silverlight toolkit and I'm trying to get the parent of a selected node. The TreeView is bound to a series of objects, so directly working with a TreeViewItem appears to be out of the question.
<toolkit:TreeView SelectedItemChanged="DoStuff" DisplayMemberPath="Name" ItemsSource="{Binding MyCollection}">
<toolkit:TreeView.ItemTemplate>
<common:HierarchicalDataTemplate ItemsSource="{Binding MySubCollection}">
<StackPanel>
<TextBlock Text="{Binding Name}" />
</StackPanel>
</common:HierarchicalDataTemplate>
</toolkit:TreeView.ItemTemplate>
</toolkit:TreeView>
Is there a way to fetch the parent of an item selected in the DoStuff event?
As long as you've downloaded the latest Silverlight Toolkit then this is easy using the TreeViewExtensions that are included.
Download the Silverlight Toolkit and install.
Add a reference to System.Windows.Controls.Toolkit (from the Silverlight Toolkit)
Use the GetParentItem() extension method, like so:
private void DoStuff(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (e.NewValue != null)
{
var parent = ((TreeView)sender).GetParentItem(e.NewValue);
//
// Do stuff with parent, this snippet updates
// a TextBlock showing the name of the current parent
if (parent != null)
{
Status.Text = parent.ToString();
}
}
}
Why don't you just have the objects in MySubcollection keep a reference to their parent?
Suppose your Tree's name is "tree", this should do the trick
((tree.SelectedItem as TreeViewItem).Parent as SomeObjectOfYours)
Justin Angel wrote about "advanced" TreeView uses, among them finding arbirtraty elements in a treeview by their bound object.
Since I am comparatively new to Silverlight, I am not sure if there is a better, more elegant way, but you could use one of his methods to find the parent.
I imagine using a call this way:
TreeViewItem trvi = ContainerFromItem(tree, tree.SelectedItem);
MySubCollectionItem parentItem = ItemFromContainer(tree, trvi.Parent); // note the "Parent" here
With the extension methods below available from somewhere:
public static TreeViewItem ContainerFromItem(this TreeView treeView, object item)
{
TreeViewItem containerThatMightContainItem = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(item);
if (containerThatMightContainItem != null)
return containerThatMightContainItem;
else
return ContainerFromItem(treeView.ItemContainerGenerator, treeView.Items, item);
}
private static TreeViewItem ContainerFromItem(ItemContainerGenerator parentItemContainerGenerator, ItemCollection itemCollection, object item)
{
foreach (object curChildItem in itemCollection)
{
TreeViewItem parentContainer = (TreeViewItem)parentItemContainerGenerator.ContainerFromItem(curChildItem);
if (parentContainer == null)
return null;
TreeViewItem containerThatMightContainItem = (TreeViewItem)parentContainer.ItemContainerGenerator.ContainerFromItem(item);
if (containerThatMightContainItem != null)
return containerThatMightContainItem;
TreeViewItem recursionResult = ContainerFromItem(parentContainer.ItemContainerGenerator, parentContainer.Items, item);
if (recursionResult != null)
return recursionResult;
}
return null;
}
public static object ItemFromContainer(this TreeView treeView, TreeViewItem container)
{
TreeViewItem itemThatMightBelongToContainer = (TreeViewItem)treeView.ItemContainerGenerator.ItemFromContainer(container);
if (itemThatMightBelongToContainer != null)
return itemThatMightBelongToContainer;
else
return ItemFromContainer(treeView.ItemContainerGenerator, treeView.Items, container);
}
private static object ItemFromContainer(ItemContainerGenerator parentItemContainerGenerator, ItemCollection itemCollection, TreeViewItem container)
{
foreach (object curChildItem in itemCollection)
{
TreeViewItem parentContainer = (TreeViewItem)parentItemContainerGenerator.ContainerFromItem(curChildItem);
if (parentContainer == null)
return null;
TreeViewItem itemThatMightBelongToContainer = (TreeViewItem)parentContainer.ItemContainerGenerator.ItemFromContainer(container);
if (itemThatMightBelongToContainer != null)
return itemThatMightBelongToContainer;
TreeViewItem recursionResult = ItemFromContainer(parentContainer.ItemContainerGenerator, parentContainer.Items, container) as TreeViewItem;
if (recursionResult != null)
return recursionResult;
}
return null;
}

Resources