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;
}
Related
I have a datagrid with quite a few columns. Now normally user can press TAB to cycle between controls in the DataGrid cells. Data grid cells have been set to IsTabStop = false, so it's skipping focusing on the the cell itself and it just falls through until focus is given to an actual control.
This seems to NOT WORK if the user presses Enter because they want to work within that same column. DataGrid will pass the focus to the next cell in the COLUMN, BUT it will also ignore the IsTabStop setting, so it just gives focus to the cell itself not the control inside.
Any ideas how to fix this.
Short example
<DataGrid AutoGenerateColumns="False" Name="grid1" >
<DataGrid.Resources>
<Style TargetType="{x:Type Border}" x:Key="coloredBorder">
<Setter Property="Background" Value="Yellow"/>
</Style>
<Style TargetType="DataGridCell">
<Setter Property="IsTabStop" Value="False"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding A, Mode=OneWay}" Header="A"/>
<DataGridTemplateColumn Header="B">
<DataGridTemplateColumn.CellStyle>
<Style TargetType="{x:Type DataGridCell}" BasedOn="{StaticResource {x:Type DataGridCell}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border BorderBrush="Black" BorderThickness="0,0,1,0" Width="20" Height="20" Style="{DynamicResource coloredBorder}"/>
<ComboBox Grid.Column="1" SelectedItem="{Binding A, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding MenuItems, ElementName=window}"></ComboBox>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGridTemplateColumn.CellStyle>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Also I have appended the example WPF app for this here. To reproduce, clck on one of the comboboxes to give it focus. Pres tab and you'll see that the next combo box gets focus normally. If I added more than one combo box column the combobox to the right would get focus.
BUT if you press enter, the focus would go to the cell below, but it won't then skip the cell and go for the cell content. Instead cell is highlighted.
Like so:
Based on reply I got on MSDN:
I solved this by making an attached property:
using System.Windows;
using System.Windows.Input;
namespace GridTabbing
{
public class EnterKeySample
{
public static bool GetIsEnabled(DependencyObject obj)
{
return (bool)obj.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject obj, bool value)
{
obj.SetValue(IsEnabledProperty, value);
}
static void ue_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
var ue = e.OriginalSource as FrameworkElement;
if (e.Key == Key.Enter)
{
e.Handled = true;
ue.MoveFocus(new TraversalRequest(FocusNavigationDirection.Down));
}
}
private static void ue_Unloaded(object sender, RoutedEventArgs e)
{
var ue = sender as FrameworkElement;
if (ue == null) return;
ue.Unloaded -= ue_Unloaded;
ue.PreviewKeyDown -= ue_PreviewKeyDown;
}
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool),
typeof(EnterKeySample), new UIPropertyMetadata(false, IsEnabledChanged));
static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ue = d as FrameworkElement;
if (ue == null) return;
if ((bool)e.NewValue)
{
ue.Unloaded += ue_Unloaded;
ue.PreviewKeyDown += ue_PreviewKeyDown;
}
else
{
ue.PreviewKeyDown -= ue_PreviewKeyDown;
}
}
}
}
The weird bit is that the only thing this is doing is telling WPF to pass focus DOWN when user presses Enter, which I would have assumed was already happening but obviously isn't.
I would like to modify the selection behavior of the DataGrid in the following way. Normally when you have multiple rows selected, and then you click one of the items already selected, the selection is reset to only the clicked item. I would like to change it such that if one of the multi-selected rows is clicked without any keyboard modifiers, the selection is not modified. The goal of this is to allow a multi-item drag-drop.
I noticed that when aforementioned default behavior is activated, the call stack includes:
at System.Windows.Controls.DataGrid.OnSelectionChanged(SelectionChangedEventArgs e)
at System.Windows.Controls.Primitives.Selector.SelectionChanger.End()
at System.Windows.Controls.DataGrid.MakeFullRowSelection(ItemInfo info, Boolean allowsExtendSelect, Boolean allowsMinimalSelect)
at System.Windows.Controls.DataGrid.HandleSelectionForCellInput(DataGridCell cell, Boolean startDragging, Boolean allowsExtendSelect, Boolean allowsMinimalSelect)
at System.Windows.Controls.DataGridCell.OnAnyMouseLeftButtonDown(MouseButtonEventArgs e)
at System.Windows.RoutedEventArgs.InvokeHandler(Delegate handler, Object target)
at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs)
at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised)
at System.Windows.UIElement.ReRaiseEventAs(DependencyObject sender, RoutedEventArgs args, RoutedEvent newEvent)
at System.Windows.UIElement.OnMouseDownThunk(Object sender, MouseButtonEventArgs e)
at System.Windows.RoutedEventArgs.InvokeHandler(Delegate handler, Object target)
Therefore it looks like I should be able to modify the behavior by overriding DataGridCell.OnMouseLeftButtonDown, something like this:
class MultiDragDataGridCell : DataGridCell
{
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
// This allows users to click-and-drag a multi-selection by handling the event before
// the default behavior (deselecting everything but the clicked cell) kicks in.
if (IsSelected && Keyboard.Modifiers == ModifierKeys.None)
{
e.Handled = true;
}
base.OnMouseLeftButtonDown(e);
}
}
However, I'm having trouble getting the DataGrid to create a MultiDragDataGridCell instead of a normal DataGridCell, since the class that instantiates DataGridCell is internal. Anyone know how I can achieve that, or if there's another way of achieving the behavior I want?
Other things I tried:
Styling the DataGridCell to add a handler to MouseLeftButtonDown. This doesn't work because it executes after the selection has already changed.
Styling the DataGridCell to add a handler to PreviewMouseLeftButtonDown. This works, but it prevents me from clicking any buttons, etc. inside the cell.
Note: This answer only tries to provide a solution to following issue mentioned in the question; not how to override the grid's selection behavior. I am hoping that once you have a custom DataGridCell in place, it can be a good starting point for what you are trying to do.
However, I'm having trouble getting the DataGrid to create a MultiDragDataGridCell instead of a normal DataGridCell, since the class that instantiates DataGridCell is internal. Anyone know how I can achieve that..
Solution: In order to ensure that the DataGrid uses your custom DataGridCell - you need to re-template your DataGridRow to use an extended version of DataGridCellsPresenter which in-turn will provide your custom DataGridCell.
Please refer following sample code:
Extending DataGrid controls
public class ExtendedDataGrid : DataGrid
{
protected override DependencyObject GetContainerForItemOverride()
{
//This provides the DataGrid with a customized version for DataGridRow
return new ExtendedDataGridRow();
}
}
public class ExtendedDataGridRow : DataGridRow { }
public class ExtendedDataGridCellsPresenter : System.Windows.Controls.Primitives.DataGridCellsPresenter
{
protected override DependencyObject GetContainerForItemOverride()
{
//This provides the DataGrid with a customized version for DataGridCell
return new ExtendedDataGridCell();
}
}
public class ExtendedDataGridCell : DataGridCell
{
// Your custom/overridden implementation can be added here
}
Re-template DataGridRow in XAML (a more comprehensive template can be found at this link - I am only using a watered-down version of it for sake of readability).
<Style TargetType="{x:Type local:ExtendedDataGridRow}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ExtendedDataGridRow}">
<Border x:Name="DGR_Border"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True">
<SelectiveScrollingGrid>
<SelectiveScrollingGrid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</SelectiveScrollingGrid.ColumnDefinitions>
<SelectiveScrollingGrid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</SelectiveScrollingGrid.RowDefinitions>
<!-- Make sure to register your custom DataGridCellsPresenter here as following -->
<local:ExtendedDataGridCellsPresenter Grid.Column="1"
ItemsPanel="{TemplateBinding ItemsPanel}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<DataGridDetailsPresenter Grid.Column="1"
Grid.Row="1"
Visibility="{TemplateBinding DetailsVisibility}"
SelectiveScrollingGrid.SelectiveScrollingOrientation=
"{Binding AreRowDetailsFrozen,
ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical},
Converter={x:Static DataGrid.RowDetailsScrollingConverter},
RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
<DataGridRowHeader Grid.RowSpan="2"
SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical"
Visibility="{Binding HeadersVisibility,
ConverterParameter={x:Static DataGridHeadersVisibility.Row},
Converter={x:Static DataGrid.HeadersVisibilityConverter},
RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
</SelectiveScrollingGrid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And, your extended DataGrid's visual tree have the custom datagrid-cells:
Also, please note, it is not mandatory to extend DataGrid, or DataGridRow to provide a custom DataGridCell - you can achieve the same result by just extending DataGridCellsPresenter (and, updating DataGridRow's control-template to use the extended version)
Actually you had a solution: make a styling for DataGridCell and set an event handler, but I suppose there was a logical error in your event handler: you have set e.Handled to true, if DataGridCell was selected, so the inner controls could not be manipulated, because the default behavior for DataGrid is first select/unselect the row/cell(and only then manipulate the inner controls), so in case you have multiple selection the clicked upon row/cell was selected, so actually you only have needed to prevent selection of row/cell clicked upon in case of multiple selection.
I suppose this should work as you have expected:
<DataGrid.Resources>
<Style TargetType="DataGridCell">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="PreviewMouseDown"/>
</Style>
</DataGrid.Resources>
private void PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
var cell = sender as DataGridCell; if (cell == null) { return; }
DataGrid parGrid = null;
var visParent = VisualTreeHelper.GetParent(cell);
while (parGrid==null && visParent != null)
{
parGrid = visParent as DataGrid;
visParent = VisualTreeHelper.GetParent(visParent);
}
if (parGrid==null) { return; }
e.Handled = cell.IsSelected && Keyboard.Modifiers == ModifierKeys.None && parGrid.SelectedItems.Count > 1;
}
The only thing I could come up with feels like a big hack, so better don't use it as is. But it might be a starting point for finding your own solution.
Basic ideas:
Execute some event handlers even on handled events with EventManager.RegisterClassHandler. This needs some refinement or you end up messing with all cells in the whole application
Register for cell selection restore when left mouse click on selected cell without modifiers
Consider drag & drop only after left mouse click on selected cell (otherwise the user experience becomes REALLY strange for this combination of requirements)
Restore selected cells if previously registered and cells are unselected
Remove cell selection restore registration after restoring or when mouse does other things (mouse up or mouse move)
Customized data grid code:
public class MyDataGrid : DataGrid
{
static MyDataGrid()
{
EventManager.RegisterClassHandler(typeof(DataGridCell), UIElement.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(PreviewMouseLeftButtonDownHandler));
EventManager.RegisterClassHandler(typeof(DataGridCell), UIElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler(MouseLeftButtonUpHandler), true);
EventManager.RegisterClassHandler(typeof(DataGridCell), UIElement.MouseMoveEvent, new MouseEventHandler(MouseMoveHandler), true);
}
private static bool restoreNextCells = false;
private static bool isSelectedCell = false;
private static void PreviewMouseLeftButtonDownHandler(object sender, MouseButtonEventArgs e)
{
var cell = sender as DataGridCell;
isSelectedCell = cell.IsSelected;
restoreNextCells = cell.IsSelected && Keyboard.Modifiers == ModifierKeys.None;
}
private static void MouseMoveHandler(object sender, MouseEventArgs e)
{
var cell = sender as DataGridCell;
if (isSelectedCell && e.LeftButton == MouseButtonState.Pressed && cell.IsSelected && Keyboard.Modifiers == ModifierKeys.None)
{
DragDrop.DoDragDrop(cell, new ObjectDataProvider(), DragDropEffects.All);
}
restoreNextCells = false;
isSelectedCell = false;
}
private static void MouseLeftButtonUpHandler(object sender, MouseButtonEventArgs e)
{
restoreNextCells = false;
isSelectedCell = false;
}
protected override void OnSelectedCellsChanged(SelectedCellsChangedEventArgs e)
{
if (restoreNextCells && e.RemovedCells.Count > 0)
{
foreach (DataGridCellInfo item in e.RemovedCells)
{
SelectedCells.Add(item);
}
restoreNextCells = false;
}
base.OnSelectedCellsChanged(e);
}
}
Use with multi cell selection.
<local:MyDataGrid SelectionMode="Extended" SelectionUnit="Cell">
Hope I didn't leave out any important part in my explanation... ask if anything is unclear.
Is there any way to achieve something like office undo drop down (image bellow) ?
I mean, i want to highlight previous item when user mouse over item other than first ?
I tried some control from FluentRibbon but so far without luck..
In most cases designing a control like this is done in Blend. However, if you don't know how to use Blend, you can still achieve the similar results with just XAML and the code-behind, but you have to do more work.
We start by creating a class called CustomListBoxItem which inherits from ListBoxItem. Then, we define a dependency property, which is used for highlighting items in the listbox:
public class CustomListBoxItem : ListBoxItem
{
public static readonly DependencyProperty IsVirtuallySelectedProperty =
DependencyProperty.Register("IsVirtuallySelected", typeof(bool),
typeof(CustomListBoxItem),
new PropertyMetadata(false));
public CustomListBoxItem() : base()
{ }
public bool IsVirtuallySelected
{
get { return (bool)GetValue(IsVirtuallySelectedProperty); }
set { SetValue(IsVirtuallySelectedProperty, value); }
}
}
Then we add a listbox and define a style for it in XAML:
<ListBox Name="listBox" MouseMove="listBox_MouseMove" SelectionChanged="listBox_SelectionChanged">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type local:CustomListBoxItem}">
<Style.Triggers>
<Trigger Property="IsVirtuallySelected" Value="true">
<Setter Property="Background" Value="SkyBlue"/>
</Trigger>
<Trigger Property="IsVirtuallySelected" Value="false">
<Setter Property="Background" Value="White"/>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
where local is the namespace in which CustomListBoxItem is defined. This is all we need for the XAML part, the real magic happens in the code behind.
The listBox_MouseMove event handler looks like this:
private void listBox_MouseMove(object sender, MouseEventArgs e)
{
bool itemFound = false;
for (int i = 0; i < listBox.Items.Count; i++)
{
var currentItem = listBox.ItemContainerGenerator.ContainerFromIndex(i) as CustomListBoxItem;
if (currentItem == null)
continue;
// Check whether the cursor is on an item or not.
if (IsMouseOverItem(currentItem, e.GetPosition((IInputElement)currentItem)))
{
// Unselect all items before selecting the new group
listBox.Items.Cast<CustomListBoxItem>().ToList().ForEach(x => x.IsVirtuallySelected = false);
// Select the current item and the ones above it
for (int j = 0; j <= listBox.Items.IndexOf(currentItem); j++)
{
((CustomListBoxItem)listBox.Items[j]).IsVirtuallySelected = true;
}
itemFound = true;
break;
}
}
// If the item wasn't found for the mouse point, it means the pointer is not over any item, so unselect all.
if (!itemFound)
{
listBox.Items.Cast<CustomListBoxItem>().ToList().ForEach(x => x.IsVirtuallySelected = false);
}
}
And the IsMouseOverItem helper method, which is used to determine if the cursor is on an item, is defined like this:
private bool IsMouseOverItem(Visual item, Point mouseOverPoint)
{
Rect currentDescendantBounds = VisualTreeHelper.GetDescendantBounds(item);
return currentDescendantBounds.Contains(mouseOverPoint);
}
And finally the listBox_SelectedChanged event handler which acts as the click handler for the ListBox:
private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Get all the virtually selected items
List<CustomListBoxItem> selectedItems =
listBox.Items.Cast<CustomListBoxItem>().Where(x => x.IsVirtuallySelected).ToList();
if (selectedItems == null || !selectedItems.Any())
return;
// Do something with the selected items
DoCoolStuffWithSelectedItems();
// Unselsect all.
listBox.Items.Cast<CustomListBoxItem>().ToList().ForEach(x => x.IsVirtuallySelected = false);
listBox.UnselectAll();
}
And boom, we're done. We can now add some items to the ListBox in the class constructor:
public MainWindow()
{
InitializeComponent();
listBox.Items.Add(new CustomListBoxItem { Content = "hello world!" });
listBox.Items.Add(new CustomListBoxItem { Content = "wpf is cool" });
listBox.Items.Add(new CustomListBoxItem { Content = "today is tuesday..." });
listBox.Items.Add(new CustomListBoxItem { Content = "I like coffee" });
}
Note that I used a random color as the highlight color in XAML. Feel free to change it and give it a try.
Guess you need something like this:
<ControlTemplate TargetType="ListBoxItem">
<TextBlock Text="{Binding LastOperation}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger>
<DataTrigger.Binding>
<MultiBinding>
<Binding Path="MouseOverIndex"/>
<Binding Path="CurrentIndex"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Foreground" Value="Gold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</ControlTemplate>
I'm still fighting with manipulation of cell backgrounds so I'm asking a new question.
A user "H.B." wrote that I can actually set the cell style during the AutoGeneratingColumn event - Change DataGrid cell colour based on values. The problem is that I'm not sure how to do it.
What I want:
Set different background colours for each cell depending on its value. If the value is null I also want it not to be clickable (focusable I guess).
What I have / I'm trying to do:
private void mydatagrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
foreach (Cell cell in e.Column)
{
if (cell.Value < 1)
{
cell.Background = Color.Black;
cell.isFocusable = false;
}
else
{
cell.Background = Color.Pink;
}
}
}
This is just the pseudocode. Is something like this is possible during column auto-generation and if so, how can I edit my code so it will be valid?
I read about value convertors but I want to know if it's somehow possible programmatically, without writing XAML.
Please understand that I'm still a beginner to C#/WPF/DataGrid.
Solution part1:
I used the answer I accepted. Just put it into
<Window.Resources>
<local:ValueColorConverter x:Key="colorConverter"/>
<Style x:Key="DataGridCellStyle1" TargetType="{x:Type DataGridCell}">
<Setter Property="Padding" Value="5"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<Border Padding="{TemplateBinding Padding}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
<Border.Background>
<MultiBinding Converter="{StaticResource colorConverter}">
<Binding RelativeSource="{RelativeSource AncestorType=DataGridCell}" Path="Content.Text"/>
<Binding RelativeSource="{RelativeSource AncestorType=DataGridCell}" Path="IsSelected"/>
</MultiBinding>
</Border.Background>
<ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
And made for it a MultiBinding convertor so I can also set the background color for selected cells.
Problem:
Now I only have to solve the problem of setting focus of empty cells. Any hints?
<Style.Triggers>
<Trigger Property="HasContent" Value="False">
<Setter Property="Focusable" Value="False"/>
</Trigger>
</Style.Triggers>
This doesn't work. I had empty strings in the empty cells, but they are filled with ´null´s so it should work, right? Or what am I doing wrong :| ?
Solution part 2:
So the code above won't work as long as the cell value is a ´TextBox´ so I decided to find another way to deal with it which can be found in my answer here: https://stackoverflow.com/a/16673602/2296407
Thanks for trying to help me :)
I can propose two different solutions for your question: the first is "code-behind-style" (which you are asking for but personally I think it is not right approach in WPF) and more WPF-style (which more tricky but keeps code-behind clean and utilizes styles, triggers and converters)
Solution 1. Event handling and code-behind logic for coloring
First of all, the approach you've chosen will not work directly - the AutoGeneratingColumn event is meant to be used for altering the entire column appearance, not on the cell-by-cell basis. So it can be used for, say, attaching the correct style to entire column basing on it's display index or bound property.
Generally speaking, for the first time the event is raised your datagrid will not have any rows (and consequently - cells) at all. If you really need to catch the event - consider your DataGrid.LoadingRow event instead. And you will not be able to get the cells that easy :)
So, what you do: handle the LoadingRow event, get the row (it has the Item property which holds (surprisingly :)) your bound item), get the bound item, make all needed calculations, get the cell you need to alter and finally alter the style of the target cell.
Here is the code (as item I use a sample object with the int "Value" property that I use for coloring).
XAML
<DataGrid Name="mygrid" ItemsSource="{Binding Items}" AutoGenerateColumns="True" LoadingRow="DataGrid_OnLoadingRow"/>
.CS
private void DataGrid_OnLoadingRow(object sender, DataGridRowEventArgs e)
{
Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() => AlterRow(e)));
}
private void AlterRow(DataGridRowEventArgs e)
{
var cell = GetCell(mygrid, e.Row, 1);
if (cell == null) return;
var item = e.Row.Item as SampleObject;
if (item == null) return;
var value = item.Value;
if (value <= 1) cell.Background = Brushes.Red;
else if (value <= 2) cell.Background = Brushes.Yellow;
else cell.Background = Brushes.Green;
}
public static DataGridRow GetRow(DataGrid grid, int index)
{
var row = grid.ItemContainerGenerator.ContainerFromIndex(index) as DataGridRow;
if (row == null)
{
// may be virtualized, bring into view and try again
grid.ScrollIntoView(grid.Items[index]);
row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(index);
}
return row;
}
public static T GetVisualChild<T>(Visual parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
var v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T ?? GetVisualChild<T>(v);
if (child != null)
{
break;
}
}
return child;
}
public static DataGridCell GetCell(DataGrid host, DataGridRow row, int columnIndex)
{
if (row == null) return null;
var presenter = GetVisualChild<DataGridCellsPresenter>(row);
if (presenter == null) return null;
// try to get the cell but it may possibly be virtualized
var cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
if (cell == null)
{
// now try to bring into view and retreive the cell
host.ScrollIntoView(row, host.Columns[columnIndex]);
cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
}
return cell;
}
Solution 2. WPF-style
This solution uses code-behind only for value-to-color convertions (assuming that that logic of coloring is more complex than equality comparison - in that case you can use triggers and do not mess with converters).
What you do: set DataGrid.CellStyle property with style that contains a data trigger, which checks if the cell is within a desired column (basing on it's DisplayIndex) and if it is - applies background through a converter.
XAML
<DataGrid Name="mygrid" ItemsSource="{Binding Items}" AutoGenerateColumns="True">
<DataGrid.Resources>
<local:ValueColorConverter x:Key="colorconverter"/>
</DataGrid.Resources>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Column.DisplayIndex}" Value="1">
<Setter Property="Background" Value="{Binding RelativeSource={RelativeSource Self}, Path=Content.Text, Converter={StaticResource colorconverter}}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
</DataGrid>
.CS
public class ValueColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var str = value as string;
if (str == null) return null;
int intValue;
if (!int.TryParse(str, out intValue)) return null;
if (intValue <= 1) return Brushes.Red;
else if (intValue <= 2) return Brushes.Yellow;
else return Brushes.Green;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
UPD: If you need to color entire datagrid, XAML is much easier (no need to use triggers). Use the following CellStyle:
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="Background" Value="{Binding RelativeSource={RelativeSource Self}, Path=Content.Text, Converter={StaticResource colorconverter}}"/>
</Style>
</DataGrid.CellStyle>
What i meant is that you can set the CellStyle property of the column, you can not manipulate cells directly as they are not available in this event. The style can contain your conditional logic in the form of DataTriggers (will need a converter as you have "less-than" and not equals) and Setters.
Also if the logic is not specific to the columns you can set the style globally on the grid itself. The point of using the event would be to manipulate the column properties which you can not access otherwise.
I am not sure whether this property (Cell.Style) is available in your WPF Datagrid. Probably some alternative exists in your case. It has worked for WinForms datagrid.
cell.Style.BackColor = System.Drawing.Color.Black;
is there a way to mimic the behavior of ctrl+click which keeps previously selected rows selected and just adds more selected items?
by default, when clicking on each row, all previously selected rows get de-selected.
one way to achieve this, would be to override SelectionChanged event, and re-select removed rows.
void TestGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) {
foreach (var i in e.RemovedItems)
TestGrid.SelectedItems.Add(i);
}
This is not ideal however, because in some situations i would want to de-select rows (such as when clicking a toggle button in one of the columns).
This is not pretty, but it works if you can live with selecting multiple rows by dragging not working.
private void dataGrid_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
var dep = (DependencyObject)e.OriginalSource;
// iteratively traverse the visual tree
while ((dep != null) &&
!(dep is DataGridRow))
{
dep = VisualTreeHelper.GetParent(dep);
}
if (dep == null)
return;
if (dep is DataGridRow)
{
var row = dep as DataGridRow;
row.IsSelected = !row.IsSelected;
e.Handled = true;
}
}
Set the SelectionMode to DataGridSelectionMode.Extended
here's a solution that worked for me.
i removed all properties that set details visiblity (to keep everything at default)
than added the following style
<Style x:Key="VisibilityStyle" TargetType="{x:Type DataGridRow}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Visible}" Value="False">
<Setter Property="DetailsVisibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Visible}" Value="True">
<Setter Property="DetailsVisibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
assigned this resource to RowStyle
in my underlying data object, i added Visible property, and Implemented INotifyPropertyChanged interface.
now whenever i want to show/hide details, i simply manipulate the Visible property on my underlying object. this can happen from column button handler, to anywhere else in my code. works great