WPF Datagrid DataGridTemplateColumn tab focus issue - wpf

I am facing tab issue with WPF DataGrid DataGridTemplateColumn.I need to tab twice to get the focus on to the control with in the DataGridTemplateColumn.I tried below way as specified in this site for this issue. But still my problem is not resolved.
<my:DataGrid AutoGenerateColumns="False" Name="dgCB" Margin="8,32,0,0" SelectionMode="Single" GridLinesVisibility="All" FontSize="13" SelectionUnit="Cell"
KeyboardNavigation.TabNavigation="Continue" CanUserAddRows="False" CanUserDeleteRows="False"
EnableColumnVirtualization="True" VerticalAlignment="Top"
RowDetailsVisibilityMode="VisibleWhenSelected"
IsSynchronizedWithCurrentItem="True"
CanUserSortColumns="False"
CanUserReorderColumns="False"
CanUserResizeColumns="True"
CanUserResizeRows="True"
HorizontalAlignment="Left"
Width="964"
Height="416">
<my:DataGrid.Columns>
<my:DataGridTemplateColumn Header="Tis">
<my:DataGridTemplateColumn.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="KeyboardNavigation.IsTabStop" Value="False"/>
</Style>
</my:DataGridTemplateColumn.CellStyle>
<my:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Name="txtC" PBOValidation:TextBoxMaskBehavior.Mask="Integer" LostFocus="txtC_LostFocus" Text="{Binding Path=TIME}" GotKeyboardFocus="txtC_GotKeyboardFocus"></TextBox>
</DataTemplate>
</my:DataGridTemplateColumn.CellTemplate>
</my:DataGridTemplateColumn>
</my:DataGrid.Columns>
</my:DataGrid>
In code behind file I am written the event handler like below.
private void txtC_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
try
{
((TextBox)sender).SelectAll();
}
catch { }
}
private void txtC_LostFocus(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(((System.Windows.Controls.TextBox)(sender)).Text.Trim()))
{
((System.Windows.Controls.TextBox)(sender)).Text = 0.ToString();
}
}
But still my problem is not resolved and I am getting below error:
'DataGridCell' targettype does not match type of element 'DataGridCell'
Please help me to resolve my tab focus issue.

You should define a behavior on DataGrid which focus on the clicked cell on first click itself. The below behavior does the required:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
/// <summary>
/// Defines a dependency property to allow the dataGridCell to become editable in mouse single click .
/// </summary>
public class DataGridCellSingleClickEditDependency : DependencyObject
{
/// <summary>
/// The is allow single click edit property
/// </summary>
public static readonly DependencyProperty IsAllowSingleClickEditProperty = DependencyProperty.RegisterAttached("IsAllowSingleClickEdit", typeof(bool), typeof(DataGridCellSingleClickEditDependency), new PropertyMetadata(false, IsAllowSingleClickEditChanged));
/// <summary>
/// Gets or sets a value indicating whether this instance is allow single click edit.
/// </summary>
/// <value>
/// <c>true</c> if this instance is allow single click edit; otherwise, <c>false</c>.
/// </value>
public bool IsAllowSingleClickEdit
{
get
{
return (bool)this.GetValue(IsAllowSingleClickEditProperty);
}
set
{
this.SetValue(IsAllowSingleClickEditProperty, value);
}
}
/// <summary>
/// Determines whether [is allow single click edit changed] [the specified sender].
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
private static void IsAllowSingleClickEditChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
DataGridCell dataGridCell = sender as DataGridCell;
if (dataGridCell != null)
{
if (e.NewValue.Equals(true))
{
dataGridCell.GotFocus += DataGridCellGotFocusHandler;
}
else
{
dataGridCell.GotFocus -= DataGridCellGotFocusHandler;
}
}
}
/// <summary>
/// Finds the visual parent.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="element">The element.</param>
/// <returns></returns>
private static T FindVisualParent<T>(UIElement element) where T : UIElement
{
UIElement parent = element;
while (parent != null)
{
T correctlyTyped = parent as T;
if (correctlyTyped != null)
{
return correctlyTyped;
}
parent = VisualTreeHelper.GetParent(parent) as UIElement;
}
return null;
}
/// <summary>
/// Handles the GotFocus event of the AssociatedObject control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="RoutedEventArgs"/> instance containing the event data.</param>
private static void DataGridCellGotFocusHandler(object sender, RoutedEventArgs e)
{
DataGridCell cell = sender as DataGridCell;
if (cell != null && !cell.IsEditing && !cell.IsReadOnly)
{
if (!cell.IsFocused)
{
cell.Focus();
}
DataGrid dataGrid = FindVisualParent<DataGrid>(cell);
if (dataGrid != null)
{
dataGrid.BeginEdit(e);
if (dataGrid.SelectionUnit != DataGridSelectionUnit.FullRow)
{
if (!cell.IsSelected)
{
cell.IsSelected = true;
}
}
else
{
DataGridRow row = FindVisualParent<DataGridRow>(cell);
if (row != null && !row.IsSelected)
{
row.IsSelected = true;
}
}
}
}
}
}
Now to attached over DataGrid, you can define a Keyed style for DataGridCell
<Style x:Key="DataGridCellSingleClickEditStyle" TargetType="{x:Type DataGridCell}">
<Setter Property="localBehaviors:DataGridCellSingleClickEditDependency.IsAllowSingleClickEdit" Value="True" />
</Style>
Not to apply over DataGrid set the CellStyle
<DataGrid CellStyle ="{StaticResource DataGridCellSingleClickEditStyle}">
.......
</DataGrid>

Related

Tab create new row and focus new row in DataGrid

I have some problems with my data grid. My project is transforming a Delphi project to .Net. The product owner want the same behaviour for the datagrids.
When positioned on the last cell and tab or enter is hit, the following should happen:
A new row is added
The first cell in the new row is selected
Other demands for the datagrid is:
The focus should remain inside the datagrid once it has the focus (ALT + key combinations is the way to leave the datagrid again).
The datagrid is databound
The datagrid is used in MVVM
We use the .net4.0 full profile
This article had the best solution I could find.
I preferred to use an attached property rather than a behavior, since this enabled me to set it easily in the default style for DataGrid. Here's the code:
namespace SampleDataGridApp
{
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
/// <summary>
/// An attached behavior that modifies the tab behavior for a <see cref="DataGrid"/>.
/// </summary>
public static class DataGridBehavior
{
/// <summary>
/// Identifies the <c>NewLineOnTab</c> attached property.
/// </summary>
public static readonly DependencyProperty NewLineOnTabProperty = DependencyProperty.RegisterAttached(
"NewLineOnTab",
typeof(bool),
typeof(DataGridBehavior),
new PropertyMetadata(default(bool), OnNewLineOnTabChanged));
/// <summary>
/// Sets the value of the <c>NewLineOnTab</c> attached property.
/// </summary>
/// <param name="element">The <see cref="DataGrid"/>.</param>
/// <param name="value">A value indicating whether to apply the behavior.</param>
public static void SetNewLineOnTab(DataGrid element, bool value)
{
element.SetValue(NewLineOnTabProperty, value);
}
/// <summary>
/// Gets the value of the <c>NewLineOnTab</c> attached property.
/// </summary>
/// <param name="element">The <see cref="DataGrid"/>.</param>
/// <returns>A value indicating whether to apply the behavior.</returns>
public static bool GetNewLineOnTab(DataGrid element)
{
return (bool)element.GetValue(NewLineOnTabProperty);
}
/// <summary>
/// Called when the value of the <c>NewLineOnTab</c> property changes.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private static void OnNewLineOnTabChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
DataGrid d = sender as DataGrid;
if (d == null)
{
return;
}
bool newValue = (bool)e.NewValue;
bool oldValue = (bool)e.OldValue;
if (oldValue == newValue)
{
return;
}
if (oldValue)
{
d.PreviewKeyDown -= AssociatedObjectKeyDown;
}
else
{
d.PreviewKeyDown += AssociatedObjectKeyDown;
KeyboardNavigation.SetTabNavigation(d, KeyboardNavigationMode.Contained);
}
}
/// <summary>
/// Handles the <see cref="UIElement.KeyDown"/> event for a <see cref="DataGridCell"/>.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private static void AssociatedObjectKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Tab)
{
return;
}
DataGrid dg = e.Source as DataGrid;
if (dg == null)
{
return;
}
if (dg.CurrentColumn.DisplayIndex == dg.Columns.Count - 1)
{
var icg = dg.ItemContainerGenerator;
if (dg.SelectedIndex == icg.Items.Count - 2)
{
dg.CommitEdit(DataGridEditingUnit.Row, false);
}
}
}
}
}
My default style looks like this:
<Style TargetType="DataGrid">
<Setter Property="GridLinesVisibility" Value="None" />
<Setter Property="KeyboardNavigation.TabNavigation" Value="Contained" />
<Setter Property="sampleDataGridApp:DataGridBehavior.NewLineOnTab" Value="True" />
<Setter Property="IsSynchronizedWithCurrentItem" Value="True" />
</Style>
If the last column's DataGridCell has it's IsTabStop set to false like in this example the above will not work.
Here is a buggy workaround:
private static void AssociatedObjectKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Tab)
{
return;
}
DataGrid dg = e.Source as DataGrid;
if (dg == null)
{
return;
}
int offSet = 1;
var columnsReversed = dg.Columns.Reverse();
foreach (var dataGridColumn in columnsReversed)
{
// Bug: This makes the grand assumption that a readonly column's "DataGridCell" has IsTabStop == false;
if (dataGridColumn.IsReadOnly)
{
offSet++;
}
else
{
break;
}
}
if (dg.CurrentColumn.DisplayIndex == (dg.Columns.Count - offSet))
{
var icg = dg.ItemContainerGenerator;
if (dg.SelectedIndex == icg.Items.Count - 2)
{
dg.CommitEdit(DataGridEditingUnit.Row, false);
}
}
}
Ok, I have been fighting this problem for many hours now. I have tried nearly every proposed solution out there and here is what I found that works for me...
private void grid_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Tab)
{
if (grid.SelectedIndex == grid.Items.Count - 2 && grid.CurrentColumn.DisplayIndex == grid.Columns.Count - 1)
{
grid.CommitEdit(DataGridEditingUnit.Row, false);
e.Handled = true;
}
}
}
private void DataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
if (grid.SelectedIndex == grid.Items.Count - 2)
{
grid.SelectedIndex = grid.Items.Count - 1;
grid.CurrentCell = new DataGridCellInfo(grid.Items[grid.Items.Count - 1], grid.Columns[0]);
}
}
What this code does is when you tab to the last cell of the last row and press tab it will move focus to the first cell of the new row. Which is what you would expect but this is not default behavior. The default behavior is to move focus to the next control and not commit the current row edit. This is clearly a bug in the DataGrid I believe which is why all the proposed solutions have a whiff of kluge. My solution doesn't smell that good I admit, but if you agree that this is bug, I prefer this to the ridiculous default behavior.
This solution works even if the grid is sorted. The newly entered row will sort to the proper place but the focus will be put on the first column of the new row.
The only unsolved issue is that when tabbing down from the top to the last cell before the new row, tab must be entered twice before focus is moved to the new row. I looked into this quirk for a bit and finally gave up on it.

WPF Nested Scrollviewers - giving control back to parent scollviewer

This is what my control tree looks like:
<window>
<scrollviewer>
<expander>
<scrollviewer>
<grid>
</grid>
</scrollviewer>
</expander>
<expander>
<scrollviewer>
<grid>
</grid>
</scrollviewer>
</expander>
</scrollviewer>
</window>
Using the mouse wheel, the control automatically passes from parent to child scrollviewer, but when I scroll to the end of the child scrollviewer the control doesn't pass back to the parent scorllviewer. How do I achieve this?
The expander, grid and the scrollviewers are dynamically generated.
I get a similar trouble in my application. I correct it by a depency property that will catch and pass the event too his parent. This can be applied to any control that have a scroll in it. But for me, i didn't need to validate if it was at the end of the scroll to send to his parent. You will just have to add, in the OnValueChanged method, a validation for if the scroll is at the end or at the top to send to his parent.
using System.Windows.Controls;
public static class SendMouseWheelToParent
{
public static readonly DependencyProperty ScrollProperty
= DependencyProperty.RegisterAttached("IsSendingMouseWheelEventToParent",
typeof(bool),
typeof(SendMouseWheelToParent),
new FrameworkPropertyMetadata(OnValueChanged));
/// <summary>
/// Gets the IsSendingMouseWheelEventToParent for a given <see cref="TextBox"/>.
/// </summary>
/// <param name="control">
/// The <see cref="TextBox"/> whose IsSendingMouseWheelEventToParent is to be retrieved.
/// </param>
/// <returns>
/// The IsSendingMouseWheelEventToParent, or <see langword="null"/>
/// if no IsSendingMouseWheelEventToParent has been set.
/// </returns>
public static bool? GetIsSendingMouseWheelEventToParent(Control control)
{
if (control == null)
throw new ArgumentNullException("");
return control.GetValue(ScrollProperty) as bool?;
}
/// <summary>
/// Sets the IsSendingMouseWheelEventToParent for a given <see cref="TextBox"/>.
/// </summary>
/// <param name="control">
/// The <see cref="TextBox"/> whose IsSendingMouseWheelEventToParent is to be set.
/// </param>
/// <param name="IsSendingMouseWheelEventToParent">
/// The IsSendingMouseWheelEventToParent to set, or <see langword="null"/>
/// to remove any existing IsSendingMouseWheelEventToParent from <paramref name="control"/>.
/// </param>
public static void SetIsSendingMouseWheelEventToParent(Control control, bool? sendToParent)
{
if (control == null)
throw new ArgumentNullException("");
control.SetValue(ScrollProperty, sendToParent);
}
private static void OnValueChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var scrollViewer = dependencyObject as Control;
bool? IsSendingMouseWheelEventToParent = e.NewValue as bool?;
scrollViewer.PreviewMouseWheel -= scrollViewer_PreviewMouseWheel;
if (IsSendingMouseWheelEventToParent != null && IsSendingMouseWheelEventToParent != false)
{
scrollViewer.SetValue(ScrollProperty, IsSendingMouseWheelEventToParent);
scrollViewer.PreviewMouseWheel += scrollViewer_PreviewMouseWheel;
}
}
private static void scrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
var scrollview = sender as Control;
var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
eventArg.RoutedEvent = UIElement.MouseWheelEvent;
eventArg.Source = sender;
var parent = scrollview.Parent as UIElement;
parent.RaiseEvent(eventArg);
}
}
Inspired by Janick's answer and some others, I have an implementation that scrolls ancestor ScrollViewers when inner ones (including from ListView, ListBox, DataGrid) scroll past their top/bottom. The code is in my answer here to a very similar question.

How do I (fully) implement in-place editing in a ListBox?

I'm building an application where a ListBox is displaying the Description properties of its items. I want to implement the same kind of edit-in-place functionality that you'd find, say, when editing filenames in Windows Explorer, and I'm finding it to be a whole lot of work.
What I have so far is a ContextMenu that initiates the edit. It's bound to a command in the view model that sets an IsEditingDescription property. The item template is styled so that it displays the Description in a TextBlock when IsEditingDescription is false, and in a TextBox when IsEditingDescription is true. The setter on Description sets IsEditingDescription to false after setting the description.
This works remarkably well. But there are some things that it doesn't do that it should:
The user should be able to initiate the edit by pressing F2.
The user should be able to cancel the edit by pressing ESC.
The user should be able to confirm the edit by pressing ENTER.
When the user clicks a second time on the currently selected item, it should initiate the edit.
The text of the description should be selected when the text box appears
I think I can handle the first three items with commands and key bindings, though I don't really understand how to do key bindings yet. But I can't really figure out an MVVMish way to do the other two. Is there one?
this was something I was having to do over and over so I decided to extend the ListView control and added ItemKeyDown, ItemKeyUp and ItemDoubleClick events which are fired where those events occur when an item in the ListView is in focus. The ListView is derived a ListBox so you should be able to port it over pretty easily.
/Fx/ListView.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Controls;
namespace Fx
{
public static class Func
{
/// <summary>
/// Finds a specific type of parent up the visual tree.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="dep"></param>
/// <returns></returns>
public static T FindParent<T>(this object obj)
{
DependencyObject dep = obj as DependencyObject;
while ((dep != null) && !(dep is T))
{
dep = VisualTreeHelper.GetParent(dep);
}
return (dep != null) ? (T)Convert.ChangeType(dep, typeof(T)) : default(T);
}
}
public class ListView:System.Windows.Controls.ListView
{
#region public event KeyboardEventHandler ItemKeyDown;
/// <summary>
/// Occurs when a key is pressed when a ListViewItem in this
/// ListView is in focus.
/// </summary>
public event KeyEventHandler ItemKeyDown;
/// <summary>
/// Raises the ItemKeyDown event for a ListViewitem in this ListView.
/// </summary>
/// <param name="item"></param>
/// <param name="e"></param>
public void OnItemKeyDown(ListViewItem item, KeyEventArgs e)
{
if (ItemKeyDown != null)
ItemKeyDown(item, e);
}
/// <summary>
/// Handle they KeyDown event on the ListView, find the related
/// ListViewItem and raise the ItemKeyDown event respectively.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ListView_KeyDown(object sender, KeyEventArgs e)
{
ListViewItem item = Func.FindParent<ListViewItem>(e.OriginalSource);
if (item != null)
OnItemKeyDown(item, e);
}
#endregion
#region public event KeyboardEventHandler ItemKeyUp;
/// <summary>
/// Occurs when a key is released when a ListViewItem in this
/// ListView is in focus.
/// </summary>
public event KeyEventHandler ItemKeyUp;
/// <summary>
/// Raises the ItemKeyUp event for a ListViewitem in this ListView.
/// </summary>
/// <param name="item"></param>
/// <param name="e"></param>
public void OnItemKeyUp(ListViewItem item, KeyEventArgs e)
{
if (ItemKeyUp != null)
ItemKeyUp(item, e);
}
/// <summary>
/// Handle they KeyUp event on the ListView, find the related
/// ListViewItem and raise the ItemKeyUp event respectively.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ListView_KeyUp(object sender, KeyEventArgs e)
{
ListViewItem item = Func.FindParent<ListViewItem>(e.OriginalSource);
if (item != null)
OnItemKeyUp(item, e);
}
#endregion
#region public event MouseButtonEventHandler ItemDoubleClick;
/// <summary>
/// Occurs when a ListViewItem in this Listview is double clicked.
/// </summary>
public event MouseButtonEventHandler ItemDoubleClick;
/// <summary>
/// Raise the ItemDoubleClick event for a ListViewItem.
/// </summary>
/// <param name="item"></param>
/// <param name="e"></param>
public void OnItemDoubleClick(ListViewItem item, MouseButtonEventArgs e)
{
if (ItemDoubleClick != null)
ItemDoubleClick(item, e);
}
/// <summary>
/// Handle the MouseDoubleClick event for the ListView, find the related
/// ListViewItem and raise the ItemDoubleClick event respectively.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ListView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
ListViewItem item = Func.FindParent<ListViewItem>(e.OriginalSource);
if (item != null)
OnItemDoubleClick(item, e);
}
#endregion
public ListView()
{
MouseDoubleClick += new MouseButtonEventHandler(ListView_MouseDoubleClick);
KeyDown += new KeyEventHandler(ListView_KeyDown);
KeyUp += new KeyEventHandler(ListView_KeyUp);
}
}
}
Now to use it...
/Pages/EmployeesPage.xaml
<UserControl x:Class="TestApp.Pages.EmployeesPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:fx="clr-namespace:Fx"
Width="800" Height="450">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="List Items" FontWeight="Bold" FontSize="18" />
<!-- here is the main part -->
<fx:ListView x:Name="EmployeesList" ItemDoubleClick="EmployeesList_ItemDoubleClick" ItemKeyDown="EmployeesList_ItemKeyDown" />
<!-- main part ends here -->
</StackPanel>
</Grid>
</UserControl>
/Pages/EmployeesPage.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace TestApp.Pages
{
/// <summary>
/// Interaction logic for EmployeesPage.xaml
/// </summary>
public partial class EmployeesPage : UserControl
{
public EmployeesPage()
{
InitializeComponent();
// Fill the ListView with data...
// EmployeesList.ItemsSource = SomeObservableCollectionOrDataSource
}
private void EmployeesList_ItemDoubleClick(object sender, MouseButtonEventArgs e)
{
MessageBox.Show("an item was double clicked");
ListViewItem item = sender as ListViewItem;
// entityType obj = item.DataContext as entityType
// you can begin editing here
}
private void EmployeesList_ItemKeyDown(object sender, KeyEventArgs e)
{
MessageBox.Show(e.Key.ToString() + " key was pressed on an item");
ListViewItem item = sender as ListViewItem;
// entityType obj = item.DataContext as entityType
if (e.Key == Key.F2)
{
// begin editing here
}
else if (e.Key == Key.Enter)
{
// end editing here
}
else if (e.Key == Key.Escape)
{
// cancel editing here
}
}
}
}
Hope this is of at least some use to you.. Good luck.
As for ensuring the text is selected on entering edit mode, I previously used the following behaviour attached to textboxes in another project of mine. It is in VB, but should be pretty straight-forward.
Public Class SelectAllOnFocusTextboxBehavior
Inherits Behavior(Of TextBox)
Protected Overrides Sub OnAttached()
MyBase.OnAttached()
AddHandler AssociatedObject.GotKeyboardFocus, AddressOf AssociatedObjectGotKeyboardFocus
End Sub
Protected Overrides Sub OnDetaching()
MyBase.OnDetaching()
RemoveHandler AssociatedObject.GotKeyboardFocus, AddressOf AssociatedObjectGotKeyboardFocus
End Sub
Private Sub AssociatedObjectGotKeyboardFocus(ByVal sender As Object, ByVal e As KeyboardFocusChangedEventArgs)
AssociatedObject.SelectAll()
End Sub
End Class

WPF ContextMenu.ItemsSource not resolving from binding

I have the following XAML on a ToolBar:
<emsprim:SplitButton Mode="Split">
<emsprim:SplitButton.Content>
<Image Source="images/16x16/Full Extent 1.png" />
</emsprim:SplitButton.Content>
<emsprim:SplitButton.ContextMenu>
<ContextMenu ItemsSource="{Binding CommandGroups[ZoomToDefinedExtentsCmds]}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding Command}" />
<Setter Property="CommandParameter" Value="{Binding ViewID}" />
<Setter Property="Header" Value="{Binding Name}" />
<Setter Property="Icon" Value="{Binding Icon}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</emsprim:SplitButton.ContextMenu>
</emsprim:SplitButton>
where CommandGroups[ZoomToDefinedExtentsCmds] is an IEnumerable of CommandViewModels. Problem is, when I click on the button, I do not see the list of menu items. However, if I bind the same Datacontext to a Menu, like this:
<MenuItem ItemsSource="{Binding CommandGroups[ZoomToDefinedExtentsCmds]}"
Header="Zoom To"
Margin="5,1,5,0" >
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding Command}" />
<Setter Property="CommandParameter" Value="{Binding CommandParameter}" />
<Setter Property="Header" Value="{Binding Name}" />
<Setter Property="Icon" Value="{Binding Icon}" />
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
I do get the list of MenuItems. Any ideas on what is going on here as there is no binding error in the output VS window. BTW, code for SplitButton is listed below:
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Markup;
using System.Diagnostics;
namespace Controls.Dictionary.Primitives
{
/// <summary>
/// Implemetation of a Split Button
/// </summary>
[TemplatePart(Name = "PART_DropDown", Type = typeof(Button))]
[ContentProperty("Items")]
[DefaultProperty("Items")]
public class SplitButton : Button
{
// AddOwner Dependency properties
public static readonly DependencyProperty PlacementProperty;
public static readonly DependencyProperty PlacementRectangleProperty;
public static readonly DependencyProperty HorizontalOffsetProperty;
public static readonly DependencyProperty VerticalOffsetProperty;
/// <summary>
/// Static Constructor
/// </summary>
static SplitButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SplitButton), new FrameworkPropertyMetadata(typeof(SplitButton)));
// AddOwner properties from the ContextMenuService class, we need callbacks from these properties
// to update the Buttons ContextMenu properties
PlacementProperty = ContextMenuService.PlacementProperty.AddOwner(typeof(SplitButton), new FrameworkPropertyMetadata(PlacementMode.MousePoint, OnPlacementChanged));
PlacementRectangleProperty = ContextMenuService.PlacementRectangleProperty.AddOwner(typeof(SplitButton), new FrameworkPropertyMetadata(Rect.Empty, OnPlacementRectangleChanged));
HorizontalOffsetProperty = ContextMenuService.HorizontalOffsetProperty.AddOwner(typeof(SplitButton), new FrameworkPropertyMetadata(0.0, OnHorizontalOffsetChanged));
VerticalOffsetProperty = ContextMenuService.VerticalOffsetProperty.AddOwner(typeof(SplitButton), new FrameworkPropertyMetadata(0.0, OnVerticalOffsetChanged));
}
/*
* Properties
*
*/
/// <summary>
/// The Split Button's Items property maps to the base classes ContextMenu.Items property
/// </summary>
public ItemCollection Items
{
get
{
EnsureContextMenuIsValid();
return this.ContextMenu.Items;
}
}
/*
* Dependancy Properties & Callbacks
*
*/
/// <summary>
/// Placement of the Context menu
/// </summary>
public PlacementMode Placement
{
get { return (PlacementMode)GetValue(PlacementProperty); }
set { SetValue(PlacementProperty, value); }
}
/// <summary>
/// Placement Property changed callback, pass the value through to the buttons context menu
/// </summary>
private static void OnPlacementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SplitButton s = d as SplitButton;
if (s == null) return;
s.EnsureContextMenuIsValid();
s.ContextMenu.Placement = (PlacementMode)e.NewValue;
}
/// <summary>
/// PlacementRectangle of the Context menu
/// </summary>
public Rect PlacementRectangle
{
get { return (Rect)GetValue(PlacementRectangleProperty); }
set { SetValue(PlacementRectangleProperty, value); }
}
/// <summary>
/// PlacementRectangle Property changed callback, pass the value through to the buttons context menu
/// </summary>
private static void OnPlacementRectangleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SplitButton s = d as SplitButton;
if (s == null) return;
s.EnsureContextMenuIsValid();
s.ContextMenu.PlacementRectangle = (Rect)e.NewValue;
}
/// <summary>
/// HorizontalOffset of the Context menu
/// </summary>
public double HorizontalOffset
{
get { return (double)GetValue(HorizontalOffsetProperty); }
set { SetValue(HorizontalOffsetProperty, value); }
}
/// <summary>
/// HorizontalOffset Property changed callback, pass the value through to the buttons context menu
/// </summary>
private static void OnHorizontalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SplitButton s = d as SplitButton;
if (s == null) return;
s.EnsureContextMenuIsValid();
s.ContextMenu.HorizontalOffset = (double)e.NewValue;
}
/// <summary>
/// VerticalOffset of the Context menu
/// </summary>
public double VerticalOffset
{
get { return (double)GetValue(VerticalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
/// <summary>
/// VerticalOffset Property changed callback, pass the value through to the buttons context menu
/// </summary>
private static void OnVerticalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SplitButton s = d as SplitButton;
if (s == null) return;
s.EnsureContextMenuIsValid();
s.ContextMenu.VerticalOffset = (double)e.NewValue;
}
/// <summary>
/// Defines the Mode of operation of the Button
/// </summary>
/// <remarks>
/// The SplitButton two Modes are
/// Split (default), - the button has two parts, a normal button and a dropdown which exposes the ContextMenu
/// Dropdown - the button acts like a combobox, clicking anywhere on the button opens the Context Menu
/// </remarks>
public SplitButtonMode Mode
{
get { return (SplitButtonMode)GetValue(ModeProperty); }
set { SetValue(ModeProperty, value); }
}
public static readonly DependencyProperty ModeProperty = DependencyProperty.Register("Mode", typeof(SplitButtonMode), typeof(SplitButton), new FrameworkPropertyMetadata(SplitButtonMode.Split));
/*
* Methods
*
*/
/// <summary>
/// OnApplyTemplate override, set up the click event for the dropdown if present in the template
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// set up the event handlers
ButtonBase dropDown = this.Template.FindName("PART_DropDown", this) as ButtonBase;
if (dropDown != null)
dropDown.Click += DoDropdownClick;
}
/// <summary>
/// Make sure the Context menu is not null
/// </summary>
private void EnsureContextMenuIsValid()
{
if (ContextMenu == null)
ContextMenu = new ContextMenu();
}
/*
* Events
*
*/
/// <summary>
/// Event Handler for the Drop Down Button's Click event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void DoDropdownClick(object sender, RoutedEventArgs e)
{
if (Mode == SplitButtonMode.Dropdown)
return;
if (ContextMenu == null || ContextMenu.HasItems == false) return;
ContextMenu.PlacementTarget = this;
ContextMenu.IsOpen = true;
e.Handled = true;
}
}
}
Problem solved by explicitly setting ContextMenu's DataContext.
ContextMenu is not part of visual tree, therefore, does not resolve DataContext of its "parent'- is one gotcha that gets me every time.
The MenuItem object in your second code snippet, is that outside the SplitButton scope? As in, a direct child of the object container that has the CommandGroups property defined?
I ask because the ContextMenu in the first snippet will have a null DataContext, and as such will not be able to see the CommandGroups property.
I had a similar problem about a year ago, unfortunately, the only way I could solve this was to define the ContextMenu in code and inside the Execute method for a Command. Which enabled me to assign the ItemsSource in code.
To debug DataContext (and other Binding related problems like this) you should create yourself a DebugConverter like:
public class DebugConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
This will help you debug a troublesome binding issue by creating the Binding like: {Binding Converter={StaticResource debugConverter}} and by setting a break point on the return value; line.

Is it possible to use Binding in a DataGridTemplateColumn property

It seems like no matter what i do, i get AG_E_PARSER_PROPERTY_NOT_FOUND when trying to bind a property in DataGridTemplateColumn in silverlight. I've even tried tried the following
<data:DataGridTemplateColumn dataBehaviors:DataGridColumnBehaviors.BindableTextOverride="{Binding ElementName=LayoutRoot,
Path=DataContext.ColumnOneName}">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding Name, Mode=TwoWay}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
But no luck... I know the DataGridTemplateColumn does not contain a DataContext, but i don't feel like this should be the cause of the problem when I'm giving it the element and path to bind to. Any ideas?
Turns out the only way to get this to work is to implement it like DataGridBoundColumn. The idea is to bind to the binding property. This property will internally set the binding to a private DependencyProperty. When that property changes, you can perform anything needed inside the DependencyProperty Change Callback.
Here is an example:
/// <summary>
/// Represents a System.Windows.Controls.DataGrid column that can bind to a property
/// in the grid's data source. This class provides bindable properties ending with the suffix Binding.
/// These properties will affect the properties with the same name without the suffix
/// </summary>
public class DataGridBindableTemplateColumn : DataGridBoundColumn
{
/// <summary>
/// Identifies the DataGridBindableTemplateColumn.HeaderValueProperty dependency property
/// </summary>
internal static readonly DependencyProperty HeaderValueProperty =
DependencyProperty.Register("HeaderValue", typeof(object), typeof(DataGridBindableTemplateColumn),
new PropertyMetadata(null, OnHeaderValuePropertyChanged));
/// <summary>
/// Identifies the DataGridBindableTemplateColumn.VisibilityValueProperty dependency property
/// </summary>
internal static readonly DependencyProperty VisibilityValueProperty =
DependencyProperty.Register("VisibilityValue", typeof(Visibility), typeof(DataGridBindableTemplateColumn),
new PropertyMetadata(Visibility.Visible, OnVisibilityPropertyPropertyChanged));
/// <summary>
/// The callback the fires when the VisibilityValueProperty value changes
/// </summary>
/// <param name="d">The DependencyObject from which the property changed</param>
/// <param name="e">The DependencyPropertyChangedEventArgs containing the old and new value for the depenendency property that changed.</param>
private static void OnVisibilityPropertyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DataGridBindableTemplateColumn sender = d as DataGridBindableTemplateColumn;
if (sender != null)
{
sender.OnVisibilityPropertyChanged((Visibility)e.OldValue, (Visibility)e.NewValue);
}
}
/// <summary>
/// The callback the fires when the HeaderValueProperty value changes
/// </summary>
/// <param name="d">The DependencyObject from which the property changed</param>
/// <param name="e">The DependencyPropertyChangedEventArgs containing the old and new value for the depenendency property that changed.</param>
private static void OnHeaderValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DataGridBindableTemplateColumn sender = d as DataGridBindableTemplateColumn;
if (sender != null)
{
sender.OnHeaderValueChanged((object)e.OldValue, (object)e.NewValue);
}
}
private Binding _headerBinding;
private Binding _visibilityBinding;
private DataTemplate _cellEditingTemplate;
private DataTemplate _cellTemplate;
/// <summary>
/// Gets and sets the Binding object used to bind to the Header property
/// </summary>
public Binding HeaderBinding
{
get { return _headerBinding; }
set
{
if (_headerBinding != value)
{
_headerBinding = value;
if (_headerBinding != null)
{
_headerBinding.ValidatesOnExceptions = false;
_headerBinding.NotifyOnValidationError = false;
BindingOperations.SetBinding(this, HeaderValueProperty, _headerBinding);
}
}
}
}
/// <summary>
/// Gets and sets the Binding object used to bind to the Visibility property
/// </summary>
public Binding VisibilityBinding
{
get { return _visibilityBinding; }
set
{
if (_visibilityBinding != value)
{
_visibilityBinding = value;
if (_visibilityBinding != null)
{
_visibilityBinding.ValidatesOnExceptions = false;
_visibilityBinding.NotifyOnValidationError = false;
BindingOperations.SetBinding(this, VisibilityValueProperty, _visibilityBinding);
}
}
}
}
/// <summary>
/// Gets or sets the template that is used to display the contents of a cell
/// that is in editing mode.
/// </summary>
public DataTemplate CellEditingTemplate
{
get { return _cellEditingTemplate; }
set
{
if (_cellEditingTemplate != value)
{
_cellEditingTemplate = value;
}
}
}
/// <summary>
/// Gets or sets the template that is used to display the contents of a cell
/// that is not in editing mode.
/// </summary>
public DataTemplate CellTemplate
{
get { return _cellTemplate; }
set
{
if (_cellTemplate != value)
{
_cellTemplate = value;
}
}
}
/// <summary>
///
/// </summary>
/// <param name="editingElement"></param>
/// <param name="uneditedValue"></param>
protected override void CancelCellEdit(FrameworkElement editingElement, object uneditedValue)
{
editingElement = GenerateEditingElement(null, null);
}
/// <summary>
///
/// </summary>
/// <param name="cell"></param>
/// <param name="dataItem"></param>
/// <returns></returns>
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
if (CellEditingTemplate != null)
{
return (CellEditingTemplate.LoadContent() as FrameworkElement);
}
if (CellTemplate != null)
{
return (CellTemplate.LoadContent() as FrameworkElement);
}
if (!DesignerProperties.IsInDesignTool)
{
throw new Exception(string.Format("Missing template for type '{0}'", typeof(DataGridBindableTemplateColumn)));
}
return null;
}
/// <summary>
///
/// </summary>
/// <param name="cell"></param>
/// <param name="dataItem"></param>
/// <returns></returns>
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
if (CellTemplate != null)
{
return (CellTemplate.LoadContent() as FrameworkElement);
}
if (CellEditingTemplate != null)
{
return (CellEditingTemplate.LoadContent() as FrameworkElement);
}
if (!DesignerProperties.IsInDesignTool)
{
throw new Exception(string.Format("Missing template for type '{0}'", typeof(DataGridBindableTemplateColumn)));
}
return null;
}
/// <summary>
///
/// </summary>
/// <param name="editingElement"></param>
/// <param name="editingEventArgs"></param>
/// <returns></returns>
protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
{
return null;
}
/// <summary>
///
/// </summary>
/// <param name="oldValue"></param>
/// <param name="newValue"></param>
protected virtual void OnHeaderValueChanged(object oldValue, object newValue)
{
Header = newValue;
}
/// <summary>
/// I'm to lazy to write a comment
/// </summary>
/// <param name="oldValue"></param>
/// <param name="newValue"></param>
protected virtual void OnVisibilityPropertyChanged(Visibility oldValue, Visibility newValue)
{
Visibility = newValue;
}
}
XAML:
<data:DataGridBindableTemplateColumn HeaderBinding="{Binding HeaderOne, Source={StaticResource ViewModel}}"
VisibilityBinding="{Binding HeaderOneVisibility, Source={StaticResource ViewMode}}"
HeaderStyle="{StaticResource DataColumnStyle}"
MinWidth="58">
...
</data:DataGridBindableTemplateColumn>
Hope this helps anyone with the same issue... Enjoy!

Resources