Cant set Frame.Source via triggers after NavigationCommands.GoToPage is used - c#

I am trying to create a MVVM-ready custom control that extends Frame (from Navigation), with two primary goals in mind.
Firstly, I want to be able to change the Source of the frame via triggers. This way, I can change which view is used based for some cases when a property of the ViewModel changes.
Secondly, I want the views themselves to be able to change which view is used, for certain cases where nothing in the ViewModel changes. Using a Frame/Page system and calling the NavigationCommands.GoToPage Command from inside the pages seemed to be the most appropriate way to do this, since each different view can be defined as a page.
The problem I'm running into is that setting Frame.Source via triggers works perfectly fine until the first time GoToPage is used. After that, the triggers seem to have no effect. GoToPage appears to work all of the time, any time. I've been searching all day and can't find any documentation that explains this.
Anyhow, here's the implementation for my custom Frame, where all I do is bind to GoToPage and ensure Pages inherit DataContext:
public class FrameExtended : Frame
{
public FrameExtended()
{
CommandBindings.Add(new CommandBinding(NavigationCommands.GoToPage, GoToPage_Executed));
Navigated += new System.Windows.Navigation.NavigatedEventHandler(FrameExtended_Navigated);
}
void FrameExtended_Navigated(object sender, NavigationEventArgs e)
{
(Content as FrameworkElement).DataContext = this.DataContext;
}
void GoToPage_Executed(object sender, ExecutedRoutedEventArgs e)
{
if (e.Parameter is Uri) Source = e.Parameter as Uri;
else if (e.Parameter is string) Source = new Uri(e.Parameter as string, UriKind.Relative);
}
}
Here's a test case for my ViewModel, which is about as simple as a ViewModel gets:
public enum MyEnum { MyEnumVal1, MyEnumVal2, MyEnumVal3 }
public class ViewModel : INotifyPropertyChanged
{
private MyEnum enumVal = MyEnum.MyEnumVal1;
public MyEnum EnumVal
{
get { return enumVal; }
set
{
if (enumVal != value)
{
enumVal = value;
OnPropertyChanged("EnumVal");
}
}
}
protected void OnPropertyChanged(string PropertyName)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
And here's test snippet of XAML where I employ the frame and define a few triggers:
<Button Content="ChangeViewModel" Click="Button_Click"/>
<Control>
<Control.Template>
<ControlTemplate TargetType="Control">
<local:FrameExtended x:Name="MyFrame"/>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding EnumVal}" Value="MyEnumVal1">
<Setter TargetName="MyFrame" Property="Source" Value="Pages/testpage.xaml"/>
</DataTrigger>
<DataTrigger Binding="{Binding EnumVal}" Value="MyEnumVal2">
<Setter TargetName="MyFrame" Property="Source" Value="Pages/testpage2.xaml"/>
</DataTrigger>
<DataTrigger Binding="{Binding EnumVal}" Value="MyEnumVal3">
<Setter TargetName="MyFrame" Property="Source" Value="Pages/testpage3.xaml"/>
<Setter TargetName="MyFrame" Property="Background" Value="Green"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Control.Template>
</Control>
Where Button_Click refers to:
private void Button_Click(object sender, RoutedEventArgs e)
{
var vm = this.DataContext as ViewModel;
switch (vm.EnumVal)
{
case MyEnum.MyEnumVal1: vm.EnumVal = MyEnum.MyEnumVal2; break;
case MyEnum.MyEnumVal2: vm.EnumVal = MyEnum.MyEnumVal3; break;
case MyEnum.MyEnumVal3: vm.EnumVal = MyEnum.MyEnumVal1; break;
}
}
Aside from that, testpage.xaml contains the following line:
<Button Content="NEXTPAGE" Command="GoToPage" CommandParameter="Pages/testpage2.xaml"/>
Where the rest of the pages just have a TextBlock indicating which page it is.
If I click the "ChangeViewModel" button over and over again, the frame will cycle through the pages as expected (and changing its background to green when it's on page 3). Once I click the button inside testpage.xaml that calls GoToPage, the Frame switches to testpage2. After that, subsequent clicks on ChangeViewModel never change which page is displayed, but still make the Frame's background green every 3rd click (when testpage3 should show).

The workaround I've found for this issue is to use Frame.Navigate() in my GoToPage handler defined in the custom control, instead of setting the source property directly. It seems to be that setting the Source property from C# code is what prevents triggers from changing it (different priority levels, I guess). Navigate() doesn't have this issue, and it's actually what I tried first and is most likely the ideal implementation. It didn't work before because all of these implementations were in a Control Library and Navigate() was searching for my pages in the executing assembly instead. As a side note for anyone else trying to do something similar, be sure to include the assembly reference in any URI you pass to Frame.Navigate(), as it will otherwise search in the executing assembly by default (not necessarily the defining assembly, as with URIs specified in XAML). My GoToPage handler now looks like this:
void GoToPage_Executed(object sender, ExecutedRoutedEventArgs e)
{
if (e.Parameter is Uri) Navigate(e.Parameter as Uri);
else if (e.Parameter is string) Navigate(new Uri(e.Parameter as string, UriKind.RelativeOrAbsolute));
}

Related

UserControl with custom ItemsSource doesn't detect changes

I have simple UserControl where is defined property ItemsSource
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(Dictionary<string, object>), typeof(UserControl1), new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(UserControl1.OnItemsSourceChanged)));
public Dictionary<string, object> ItemsSource
{
get { return (Dictionary<string, object>)GetValue(ItemsSourceProperty); }
set
{
SetValue(ItemsSourceProperty, value);
}
}
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UserControl1 control = (UserControl1)d;
control.DisplayInControl();
}
I want to make this property update dynamically, but I am wondered why OnItemsSourceChanged doesn't fired every time when something happend with ItemsSource. So I am upset.
I've tried Custom ItemsSource property for a UserControl but this doesn't help or I've written bad newValueINotifyCollectionChanged_CollectionChanged function
My control is from this post CodeProject
My Code :
UserControl XAML - http://pastie.org/10606317
UserControl CodeBehind - http://pastie.org/10606322
Control Usage -
<controls:MultiSelectComboBox SelectedItems="{Binding SelectedCategories, Mode=TwoWay}" Grid.Column="0" Grid.Row="0" x:Name="CategoriesFilter" DefaultText="Category" ItemsSource="{Binding Categories }" Style="{StaticResource FiltersDropDowns}"/>
Update : I've made small step to solution. I have next style :
<Style.Triggers>
<DataTrigger Binding="{Binding ItemsSource, RelativeSource={RelativeSource Self}}" Value="{x:Null}">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
<DataTrigger Binding="{Binding ItemsSource.Count, RelativeSource={RelativeSource Self}}" Value="0">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
which I apply to my control (make control disabled if no itemSource). As I update control source on click, I see that control becomes enabled, so ItemsSource aren't empty (from start it is). So problem now is just in Redrawing control content if I understand this behaviour correctly.
If you have a dictionary as your data type, and you add or remove a value from the dictionary, then your property did not actually change. This event will only fire if you have actually set this property to reference a different dictionary.
If you need wpf to automatically detect if an item is added or removed from the dictionary, you should use an ObservableCollection.
Replace Dictionary with ObservableCollection, Dictionary won't fire the propertyChanged event when add, update, delete an item.
Write your own Dictionary to fire the propertychanged event manually.
I see that the problem is that you are building a new ItemsSource for the ComboBox control within your custom UserControl, and that you are expecting that MultiSelectCombo.ItemsSource to stay synced with your UserControl1.ItemsSource. This can't happen when simply binding a Dictionary, and it won't happen even when binding an ObservableCollection -- unless you explicitly handle it.
To accomplish what you are after, first you will need ItemsSource of your custom control to be of a type that does notify us of collection changes, such as the ObservableCollection (which I'll use for this example, as you've done in your links above). Then, you'll need to update the ItemsSource of your ComboBox within your control, to keep them in sync.
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (MultiSelectComboBox)d;
// Since we have to listen for changes to keep items synced, first check if there is an active listener to clean up.
if (e.OldValue != null)
{
((ObservableCollection<Node>)e.OldValue).CollectionChanged -= OnItemsSource_CollectionChanged;
}
// Now initialize with our new source.
if (e.NewValue != null)
{
((ObservableCollection<Node>)e.NewValue).CollectionChanged += OnItemsSource_CollectionChanged;
control.DisplayInControl();
}
}
private void OnItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
DisplayInControl();
}
Now, that said, the solution above is for a more generic case, where you might need to do something with the ItemsSource given to your custom control before passing it onto your MultiSelectCombo.ItemsSource. In your current case, however, you are simply building a collection of Node to exactly match the given collection of Node. If that's guaranteed to be the case, your solution can be much, much simpler:
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (MultiSelectComboBox)d;
control.MultiSelectCombo.ItemsSource = (IEnumerable)e.NewValue;
}
Just let the wrapped ComboBox use the ItemsSource given to your custom control.

How to catch Hyperlink click events in the container - loosly coupled way?

For start - this question is exactly what I want to achieve, but without the run-time overhead of walking each Hyperlink.
Is there a way to catch the Click event of Hyperlink in higher level (e.g. in Window) ?
PreviewMouseDown event gives me the Run element in which the Hyperlink located (via e.Source or e.OriginalSource), not the Hyperlink itself.
Edit - solution, thanks to #nit:
Why Setter Property is not applied on Hyperlink?
I think this is what you can do in PreviewMouseDown event handler:
private void Window_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Run r = e.OriginalSource as Run;
if(r != null)
{
Hyperlink hyperlink = r.Parent as Hyperlink;
if(hyperlink != null)
{
//Your code here
}
}
}
}
Or if you want that all the hyperlink click should be redirected to same command handler in your ViewModel you can add style to do this as follows:
<Style TargetType="{x:Type Hyperlink}">
<Setter Property="Command" Value="{Binding MyCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
<Setter Property="CommandParameter" Value="{Binding}"/>
</Style>
thanks

How to find out if selected by mouse or key?

I have MVVM application with a WPF TreeView on the left side of a window.
A details panel on the right changes content depending on the tree node selected.
If user selects a node, the content of details panel changes immediately.
That's desired if user clicked on the node, but I want to delay changing content if user navigates the tree using key down/up.
(Same behaviour as Windows Explorer, at least under Win XP)
I assume I have to know in my ViewModel if node has been selected via mouse or keyboard.
How can I achieve this?
Update:
This is my first post hence I'm not sure if this is the right place, but I want let the community know what I did in the meantime. Here is my own solution. I'm not an expert therefore I don't know if it's a good solution. But it works for me and I would be happy if it helps others. Bugfixes, improvements or better solutions are highly appreciated.
I created below attached property HasMouseFocus...
(First I used the MouseEnterEvent but this doesn't work well if user navigates the tree with key up/down and the mouse pointer is randomly over any navigated tree item, because in that case the details gets updated immediately.)
public static bool GetHasMouseFocus(TreeViewItem treeViewItem)
{
return (bool)treeViewItem.GetValue(HasMouseFocusProperty);
}
public static void SetHasMouseFocus(TreeViewItem treeViewItem, bool value)
{
treeViewItem.SetValue(HasMouseFocusProperty, value);
}
public static readonly DependencyProperty HasMouseFocusProperty =
DependencyProperty.RegisterAttached(
"HasMouseFocus",
typeof(bool),
typeof(TreeViewItemProperties),
new UIPropertyMetadata(false, OnHasMouseFocusChanged)
);
static void OnHasMouseFocusChanged(
DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
TreeViewItem item = depObj as TreeViewItem;
if (item == null)
return;
if (e.NewValue is bool == false)
return;
if ((bool)e.NewValue)
{
item.MouseDown += OnMouseDown;
item.MouseLeave += OnMouseLeave;
}
else
{
item.MouseDown -= OnMouseDown;
item.MouseLeave -= OnMouseLeave;
}
}
/// <summary>
/// Set HasMouseFocusProperty on model of associated element.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
static void OnMouseDown(object sender, MouseEventArgs e)
{
if (sender != e.OriginalSource)
return;
TreeViewItem item = sender as TreeViewItem;
if ((item != null) & (item.HasHeader))
{
// get the underlying model of current tree item
TreeItemViewModel header = item.Header as TreeItemViewModel;
if (header != null)
{
header.HasMouseFocus = true;
}
}
}
/// <summary>
/// Clear HasMouseFocusProperty on model of associated element.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
static void OnMouseLeave(object sender, MouseEventArgs e)
{
if (sender != e.OriginalSource)
return;
TreeViewItem item = sender as TreeViewItem;
if ((item != null) & (item.HasHeader))
{
// get the underlying model of current tree item
TreeItemViewModel header = item.Header as TreeItemViewModel;
if (header != null)
{
header.HasMouseFocus = false;
}
}
}
...and applied it to the TreeView.ItemContainerStyle
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" >
<!-- These Setters binds some properties of a TreeViewItem to the TreeViewItemViewModel. -->
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Setter Property="ToolTip" Value="{Binding Path=CognosBaseClass.ToolTip}"/>
<!-- These Setters applies attached behaviors to all TreeViewItems. -->
<Setter Property="properties:TreeViewItemProperties.PreviewMouseRightButtonDown" Value="True" />
<Setter Property="properties:TreeViewItemProperties.BringIntoViewWhenSelected" Value="True" />
<Setter Property="properties:TreeViewItemProperties.HasMouseFocus" Value="True" />
</Style>
</TreeView.ItemContainerStyle>
Where properties is the path of my attached property.
xmlns:properties="clr-namespace:WPF.MVVM.AttachedProperties;assembly=WPF.MVVM"
Then in my ViewModel, if HasMousefocusProperty is true, I update the details panel (GridView) immediately. If false I simply start a DispatcherTimer and apply the currently selected item as Tag. After an Interval of 500ms the Tick-Event applies the details, but only if the selected item is still the same as Tag.
/// <summary>
/// This property is beeing set when the selected item of the tree has changed.
/// </summary>
public TreeItemViewModel SelectedTreeItem
{
get { return Property(() => SelectedTreeItem); }
set
{
Property(() => SelectedTreeItem, value);
if (this.SelectedTreeItem.HasMouseFocus)
{
// show details for selected node immediately
ShowGridItems(value);
}
else
{
// delay showing details
this._selctedNodeChangedTimer.Stop();
this._selctedNodeChangedTimer.Tag = value;
this._selctedNodeChangedTimer.Start();
}
}
}
You can handle the OnPreviewKeyDown for your TreeView(or user control having it) and programmatically set a flag in your ViewModel and consider it while refreshing details panel -
protected override void OnPreviewKeyDown(System.Windows.Input.KeyEventArgs e)
{
switch(e.Key)
{
case Key.Up:
case Key.Down:
MyViewModel.IsUserNavigating = true;
break;
}
}
A similar approch and other solutions are mentioned in this SO question -
How can I programmatically navigate (not select, but navigate) a WPF TreeView?
Update: [In response to AalanY's comment]
I don't think there is any problem in having some code-behind in Views, that doesn't break MVVM.
In the article, WPF Apps With The Model-View-ViewModel Design Pattern, the author who is Josh Smith says:
In a well-designed MVVM architecture, the codebehind for most Views
should be empty, or, at most, only contain code that manipulates the
controls and resources contained within that view. Sometimes it is
also necessary to write code in a View's codebehind that interacts
with a ViewModel object, such as hooking an event or calling a method
that would otherwise be very difficult to invoke from the ViewModel
itself.
In my experience it's impossible to build an enterprise(of considerable size) application without having any code-behind, specially when you have to use complex controls like TreeView, DataGrid or 3'rd party controls.

Using ICommand and InputBindings consistently in DataGrid

I'm trying to create a DataGrid having the following features:
Readonly Datagrid, but provide editing capabilities through double click and separate edit form (double click on specific row)
ContextMenu which calls new/edit/delete form (right click on whole DataGrid)
Delete key which calls delete form (on specific selected row)
I thought it would be a good idea to use ICommand, so I created a DataGrid like this:
public class MyDataGrid : DataGrid {
public static readonly RoutedCommand NewEntry = new RoutedCommand();
public static readonly RoutedCommand EditEntry = new RoutedCommand();
public static readonly RoutedCommand DeleteEntry = new RoutedCommand();
public MyDataGrid() {
CommandBindings.Add(new CommandBinding(NewEntry, ..., ...));
CommandBindings.Add(new CommandBinding(EditEntry, ..., ...));
CommandBindings.Add(new CommandBinding(DeleteEntry, ..., ...));
InputBindings.Add(new InputBinding(DeleteCommand, new KeyGesture(Key.Delete)));
InputBindings.Add(new MouseBinding(EditEntry, new MouseGesture(MouseAction.LeftDoubleClick)));
// ContextMenu..working fine
}
}
I then realized, double-clicking a row isn't working, so I added this:
LoadingRow += (s, e) =>
e.Row.InputBindings.Add(new MouseBinding(EditEntry,
new MouseGesture(MouseAction.LeftDoubleClick)));
And of course the delete key isn't working either, I added this:
PreviewKeyDown += (s, e) => { if(e.Key == Key.Delete) { ... } };
Why do I have to do that? Isn't the whole point of having Commands to prevent this kind of hacking around with events? Do I miss something?
In my simple and perfect world, I want to decide in the CanExecute method whether it is appropriate to handle the command, and not subscribe to tons of different event handlers..
Usually I attach the command to the DataGridCell using a Style
Here's an example using a custom AttachedCommandBehavior
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="my:CommandBehavior.Command" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:MyView}}, Path=DataContext.ShowPopupCommand}" />
<Setter Property="my:CommandBehavior.CommandParameter" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}, Path=DataContext}" />
<Setter Property="my:CommandBehavior.Event" Value="MouseDoubleClick" />
</Style>
I can't remember why I attached it to the Cell instead of the Row, but I'm sure there was a reason. You could try attaching the event to the Row instead and see what happens.

Single click edit in WPF DataGrid

I want the user to be able to put the cell into editing mode and highlight the row the cell is contained in with a single click. By default, this is double click.
How do I override or implement this?
Here is how I resolved this issue:
<DataGrid DataGridCell.Selected="DataGridCell_Selected"
ItemsSource="{Binding Source={StaticResource itemView}}">
<DataGrid.Columns>
<DataGridTextColumn Header="Nom" Binding="{Binding Path=Name}"/>
<DataGridTextColumn Header="Age" Binding="{Binding Path=Age}"/>
</DataGrid.Columns>
</DataGrid>
This DataGrid is bound to a CollectionViewSource (Containing dummy Person objects).
The magic happens there : DataGridCell.Selected="DataGridCell_Selected".
I simply hook the Selected Event of the DataGrid cell, and call BeginEdit() on the DataGrid.
Here is the code behind for the event handler :
private void DataGridCell_Selected(object sender, RoutedEventArgs e)
{
// Lookup for the source to be DataGridCell
if (e.OriginalSource.GetType() == typeof(DataGridCell))
{
// Starts the Edit on the row;
DataGrid grd = (DataGrid)sender;
grd.BeginEdit(e);
}
}
The answer from Micael Bergeron was a good start for me to find a solution thats working for me. To allow single-click editing also for Cells in the same row thats already in edit mode i had to adjust it a bit. Using SelectionUnit Cell was no option for me.
Instead of using the DataGridCell.Selected Event which is only fired for the first time a row's cell is clicked, i used the DataGridCell.GotFocus Event.
<DataGrid DataGridCell.GotFocus="DataGrid_CellGotFocus" />
If you do so you will have always the correct cell focused and in edit mode, but no control in the cell will be focused, this i solved like this
private void DataGrid_CellGotFocus(object sender, RoutedEventArgs e)
{
// Lookup for the source to be DataGridCell
if (e.OriginalSource.GetType() == typeof(DataGridCell))
{
// Starts the Edit on the row;
DataGrid grd = (DataGrid)sender;
grd.BeginEdit(e);
Control control = GetFirstChildByType<Control>(e.OriginalSource as DataGridCell);
if (control != null)
{
control.Focus();
}
}
}
private T GetFirstChildByType<T>(DependencyObject prop) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(prop); i++)
{
DependencyObject child = VisualTreeHelper.GetChild((prop), i) as DependencyObject;
if (child == null)
continue;
T castedProp = child as T;
if (castedProp != null)
return castedProp;
castedProp = GetFirstChildByType<T>(child);
if (castedProp != null)
return castedProp;
}
return null;
}
From: http://wpf.codeplex.com/wikipage?title=Single-Click%20Editing
XAML:
<!-- SINGLE CLICK EDITING -->
<Style TargetType="{x:Type dg:DataGridCell}">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="DataGridCell_PreviewMouseLeftButtonDown"></EventSetter>
</Style>
CODE-BEHIND:
//
// SINGLE CLICK EDITING
//
private void DataGridCell_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs 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)
{
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;
}
}
}
}
}
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;
}
The solution from http://wpf.codeplex.com/wikipage?title=Single-Click%20Editing worked great for me, but I enabled it for every DataGrid using a Style defined in a ResourceDictionary. To use handlers in resource dictionaries you need to add a code-behind file to it. Here's how you do it:
This is a DataGridStyles.xaml Resource Dictionary:
<ResourceDictionary x:Class="YourNamespace.DataGridStyles"
x:ClassModifier="public"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="DataGrid">
<!-- Your DataGrid style definition goes here -->
<!-- Cell style -->
<Setter Property="CellStyle">
<Setter.Value>
<Style TargetType="DataGridCell">
<!-- Your DataGrid Cell style definition goes here -->
<!-- Single Click Editing -->
<EventSetter Event="PreviewMouseLeftButtonDown"
Handler="DataGridCell_PreviewMouseLeftButtonDown" />
</Style>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Note the x:Class attribute in the root element.
Create a class file. In this example it'd be DataGridStyles.xaml.cs. Put this code inside:
using System.Windows.Controls;
using System.Windows;
using System.Windows.Input;
namespace YourNamespace
{
partial class DataGridStyles : ResourceDictionary
{
public DataGridStyles()
{
InitializeComponent();
}
// The code from the myermian's answer goes here.
}
I solved it by adding a trigger that sets IsEditing property of the DataGridCell to True when the mouse is over it. It solved most of my problems. It works with comboboxes too.
<Style TargetType="DataGridCell">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="IsEditing" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
i prefer this way based on Dušan Knežević suggestion. you click an that's it ))
<DataGrid.Resources>
<Style TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver"
Value="True" />
<Condition Property="IsReadOnly"
Value="False" />
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="IsEditing"
Value="True" />
</MultiTrigger.Setters>
</MultiTrigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
I looking for editing cell on single click in MVVM and this is an other way to do it.
Adding behavior in xaml
<UserControl xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:myBehavior="clr-namespace:My.Namespace.To.Behavior">
<DataGrid>
<i:Interaction.Behaviors>
<myBehavior:EditCellOnSingleClickBehavior/>
</i:Interaction.Behaviors>
</DataGrid>
</UserControl>
The EditCellOnSingleClickBehavior class extend System.Windows.Interactivity.Behavior;
public class EditCellOnSingleClick : Behavior<DataGrid>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.LoadingRow += this.OnLoadingRow;
this.AssociatedObject.UnloadingRow += this.OnUnloading;
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.LoadingRow -= this.OnLoadingRow;
this.AssociatedObject.UnloadingRow -= this.OnUnloading;
}
private void OnLoadingRow(object sender, DataGridRowEventArgs e)
{
e.Row.GotFocus += this.OnGotFocus;
}
private void OnUnloading(object sender, DataGridRowEventArgs e)
{
e.Row.GotFocus -= this.OnGotFocus;
}
private void OnGotFocus(object sender, RoutedEventArgs e)
{
this.AssociatedObject.BeginEdit(e);
}
}
Voila !
I slightly edit solution from Dušan Knežević
<DataGrid.Resources>
<Style x:Key="ddlStyle" TargetType="DataGridCell">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="IsEditing" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
and apply the style to my desire column
<DataGridComboBoxColumn CellStyle="{StaticResource ddlStyle}">
There are two issues with user2134678's answer. One is very minor and has no functional effect. The other is fairly significant.
The first issueis that the GotFocus is actually being called against the DataGrid, not the DataGridCell in practice. The DataGridCell qualifier in the XAML is redundant.
The main problem I found with the answer is that the Enter key behavior is broken. Enter should move you to the next cell below the current cell in normal DataGrid behavior. However, what actually happens behind the scenes is GotFocus event will be called twice. Once upon the current cell losing focus, and once upon the new cell gaining focus. But as long as BeginEdit is called on that first cell, the next cell will never be activated. The upshot is that you have one-click editing, but anyone who isn't literally clicking on the grid is going to be inconvenienced, and a user-interface designer should not assume that all users are using mouses. (Keyboard users can sort of get around it by using Tab, but that still means they're jumping through hoops that they shouldn't need to.)
So the solution to this problem? Handle event KeyDown for the cell and if the Key is the Enter key, set a flag that stops BeginEdit from firing on the first cell. Now the Enter key behaves as it should.
To begin with, add the following Style to your DataGrid:
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridCell}" x:Key="SingleClickEditingCellStyle">
<EventSetter Event="KeyDown" Handler="DataGridCell_KeyDown" />
</Style>
</DataGrid.Resources>
Apply that style to "CellStyle" property the columns for which you want to enable one-click.
Then in the code behind you have the following in your GotFocus handler (note that I'm using VB here because that's what our "one-click data grid requesting" client wanted as the development language):
Private _endEditing As Boolean = False
Private Sub DataGrid_GotFocus(ByVal sender As Object, ByVal e As RoutedEventArgs)
If Me._endEditing Then
Me._endEditing = False
Return
End If
Dim cell = TryCast(e.OriginalSource, DataGridCell)
If cell Is Nothing Then
Return
End If
If cell.IsReadOnly Then
Return
End If
DirectCast(sender, DataGrid).BeginEdit(e)
.
.
.
Then you add your handler for the KeyDown event:
Private Sub DataGridCell_KeyDown(ByVal sender As Object, ByVal e As KeyEventArgs)
If e.Key = Key.Enter Then
Me._endEditing = True
End If
End Sub
Now you have a DataGrid that hasn't changed any fundamental behavior of the out-of-the-box implementation and yet supports single-click editing.
Several of these answers inspired me, as well as this blog post, yet each answer left something wanting. I combined what seemed the best parts of them and came up with this fairly elegant solution that seems to get the user experience exactly right.
This uses some C# 9 syntax, which works fine everywhere though you may need to set this in your project file:
<LangVersion>9</LangVersion>
First, we get down from 3 clicks to 2 with this:
Add this class to your project:
public static class WpfHelpers
{
internal static void DataGridPreviewMouseLeftButtonDownEvent(object sender, RoutedEventArgs e)
{
// The original source for this was inspired by https://softwaremechanik.wordpress.com/2013/10/02/how-to-make-all-wpf-datagrid-cells-have-a-single-click-to-edit/
DataGridCell? cell = e is MouseButtonEventArgs { OriginalSource: UIElement clickTarget } ? FindVisualParent<DataGridCell>(clickTarget) : null;
if (cell is { IsEditing: false, IsReadOnly: false })
{
if (!cell.IsFocused)
{
cell.Focus();
}
if (FindVisualParent<DataGrid>(cell) is DataGrid dataGrid)
{
if (dataGrid.SelectionUnit != DataGridSelectionUnit.FullRow)
{
if (!cell.IsSelected)
{
cell.IsSelected = true;
}
}
else
{
if (FindVisualParent<DataGridRow>(cell) is DataGridRow { IsSelected: false } row)
{
row.IsSelected = true;
}
}
}
}
}
internal static T? GetFirstChildByType<T>(DependencyObject prop)
where T : DependencyObject
{
int count = VisualTreeHelper.GetChildrenCount(prop);
for (int i = 0; i < count; i++)
{
if (VisualTreeHelper.GetChild(prop, i) is DependencyObject child)
{
T? typedChild = child as T ?? GetFirstChildByType<T>(child);
if (typedChild is object)
{
return typedChild;
}
}
}
return null;
}
private static T? FindVisualParent<T>(UIElement element)
where T : UIElement
{
UIElement? parent = element;
while (parent is object)
{
if (parent is T correctlyTyped)
{
return correctlyTyped;
}
parent = VisualTreeHelper.GetParent(parent) as UIElement;
}
return null;
}
}
Add this to your App.xaml.cs file:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
EventManager.RegisterClassHandler(
typeof(DataGrid),
DataGrid.PreviewMouseLeftButtonDownEvent,
new RoutedEventHandler(WpfHelpers.DataGridPreviewMouseLeftButtonDownEvent));
}
Then we get from 2 to 1 with this:
Add this to the code behind of the page containing the DataGrid:
private void TransactionDataGrid_PreparingCellForEdit(object sender, DataGridPreparingCellForEditEventArgs e)
{
WpfHelpers.GetFirstChildByType<Control>(e.EditingElement)?.Focus();
}
And wire it up (in XAML): PreparingCellForEdit="TransactionDataGrid_PreparingCellForEdit"
I know that I am a bit late to the party but I had the same problem and came up with a different solution:
public class DataGridTextBoxColumn : DataGridBoundColumn
{
public DataGridTextBoxColumn():base()
{
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
throw new NotImplementedException("Should not be used.");
}
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
var control = new TextBox();
control.Style = (Style)Application.Current.TryFindResource("textBoxStyle");
control.FontSize = 14;
control.VerticalContentAlignment = VerticalAlignment.Center;
BindingOperations.SetBinding(control, TextBox.TextProperty, Binding);
control.IsReadOnly = IsReadOnly;
return control;
}
}
<DataGrid Grid.Row="1" x:Name="exportData" Margin="15" VerticalAlignment="Stretch" ItemsSource="{Binding CSVExportData}" Style="{StaticResource dataGridStyle}">
<DataGrid.Columns >
<local:DataGridTextBoxColumn Header="Sample ID" Binding="{Binding SampleID}" IsReadOnly="True"></local:DataGridTextBoxColumn>
<local:DataGridTextBoxColumn Header="Analysis Date" Binding="{Binding Date}" IsReadOnly="True"></local:DataGridTextBoxColumn>
<local:DataGridTextBoxColumn Header="Test" Binding="{Binding Test}" IsReadOnly="True"></local:DataGridTextBoxColumn>
<local:DataGridTextBoxColumn Header="Comment" Binding="{Binding Comment}"></local:DataGridTextBoxColumn>
</DataGrid.Columns>
</DataGrid>
As you can see I wrote my own DataGridTextColumn inheriting everything vom the DataGridBoundColumn. By overriding the GenerateElement Method and returning a Textbox control right there the Method for generating the Editing Element never gets called.
In a different project I used this to implement a Datepicker column, so this should work for checkboxes and comboboxes as well.
This does not seem to impact the rest of the datagrids behaviours..At least I haven't noticed any side effects nor have I got any negative feedback so far.
Update
A simple solution if you are fine with that your cell stays a textbox (no distinguishing between edit and non-edit mode). This way single click editing works out of the box. This works with other elements like combobox and buttons as well. Otherwise use the solution below the update.
<DataGridTemplateColumn Header="My Column header">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding MyProperty } />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Update end
Rant
I tried everything i found here and on google and even tried out creating my own versions. But every answer/solution worked mainly for textbox columns but didnt work with all other elements (checkboxes, comboboxes, buttons columns), or even broke those other element columns or had some other side effects. Thanks microsoft for making datagrid behave that ugly way and force us to create those hacks. Because of that I decided to make a version which can be applied with a style to a textbox column directly without affecting other columns.
Features
No Code behind. MVVM friendly.
It works when clicking on different textbox cells in the same or different rows.
TAB and ENTER keys work.
It doesnt affect other columns.
Sources
I used this solution and #m-y's answer and modified them to be an attached behavior.
http://wpf-tutorial-net.blogspot.com/2016/05/wpf-datagrid-edit-cell-on-single-click.html
How to use it
Add this Style.
The BasedOn is important when you use some fancy styles for your datagrid and you dont wanna lose them.
<Window.Resources>
<Style x:Key="SingleClickEditStyle" TargetType="{x:Type DataGridCell}" BasedOn="{StaticResource {x:Type DataGridCell}}">
<Setter Property="local:DataGridTextBoxSingleClickEditBehavior.Enable" Value="True" />
</Style>
</Window.Resources>
Apply the style with CellStyle to each of your DataGridTextColumns like this:
<DataGrid ItemsSource="{Binding MyData}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="My Header" Binding="{Binding Comment}" CellStyle="{StaticResource SingleClickEditStyle}" />
</DataGrid.Columns>
</DataGrid>
And now add this class to the same namespace as your MainViewModel (or a different Namespace. But then you will need to use a other namespace prefix than local). Welcome to the ugly boilerplate code world of attached behaviors.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace YourMainViewModelNameSpace
{
public static class DataGridTextBoxSingleClickEditBehavior
{
public static readonly DependencyProperty EnableProperty = DependencyProperty.RegisterAttached(
"Enable",
typeof(bool),
typeof(DataGridTextBoxSingleClickEditBehavior),
new FrameworkPropertyMetadata(false, OnEnableChanged));
public static bool GetEnable(FrameworkElement frameworkElement)
{
return (bool) frameworkElement.GetValue(EnableProperty);
}
public static void SetEnable(FrameworkElement frameworkElement, bool value)
{
frameworkElement.SetValue(EnableProperty, value);
}
private static void OnEnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DataGridCell dataGridCell)
dataGridCell.PreviewMouseLeftButtonDown += DataGridCell_PreviewMouseLeftButtonDown;
}
private static void DataGridCell_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
EditCell(sender as DataGridCell, e);
}
private static void EditCell(DataGridCell dataGridCell, RoutedEventArgs e)
{
if (dataGridCell == null || dataGridCell.IsEditing || dataGridCell.IsReadOnly)
return;
if (dataGridCell.IsFocused == false)
dataGridCell.Focus();
var dataGrid = FindVisualParent<DataGrid>(dataGridCell);
dataGrid?.BeginEdit(e);
}
private static T FindVisualParent<T>(UIElement element) where T : UIElement
{
var parent = VisualTreeHelper.GetParent(element) as UIElement;
while (parent != null)
{
if (parent is T parentWithCorrectType)
return parentWithCorrectType;
parent = VisualTreeHelper.GetParent(parent) as UIElement;
}
return null;
}
}
}
<DataGridComboBoxColumn.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="cal:Message.Attach"
Value="[Event MouseLeftButtonUp] = [Action ReachThisMethod($source)]"/>
</Style>
</DataGridComboBoxColumn.CellStyle>
public void ReachThisMethod(object sender)
{
((System.Windows.Controls.DataGridCell)(sender)).IsEditing = true;
}

Categories

Resources