I've got a WPF application with a Treeview control.
When the user clicks a node on the tree, other TextBox, ComboBox, etc. controls on the page are populated with appropriate values.
The user can then make changes to those values and save his or her changes by clicking a Save button.
However, if the user selects a different Treeview node without saving his or her changes, I want to display a warning and an opportunity to cancel that selection.
MessageBox: Continue and discard your unsaved changes? OK/Cancel http://img522.imageshack.us/img522/2897/discardsj3.gif
XAML...
<TreeView Name="TreeViewThings"
...
TreeViewItem.Unselected="TreeViewThings_Unselected"
TreeViewItem.Selected="TreeViewThings_Selected" >
Visual Basic...
Sub TreeViewThings_Unselected(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Dim OldThing As Thing = DirectCast(e.OriginalSource.DataContext, Thing)
If CancelDueToUnsavedChanges(OldThing) Then
'put canceling code here
End If
End Sub
Sub TreeViewThings_Selected(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Dim NewThing As Thing = DirectCast(e.OriginalSource.DataContext, Thing)
PopulateControlsFromThing(NewThing)
End Sub
How can I cancel those unselect/select events?
Update: I've asked a follow-up question...
How do I properly handle a PreviewMouseDown event with a MessageBox confirmation?
UPDATE
Realized I could put the logic in SelectedItemChanged instead. A little cleaner solution.
Xaml
<TreeView Name="c_treeView"
SelectedItemChanged="c_treeView_SelectedItemChanged">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
Code behind. I have some classes that is my ItemsSource of the TreeView so I made an interface (MyInterface) that exposes the IsSelected property for all of them.
private MyInterface m_selectedTreeViewItem = null;
private void c_treeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (m_selectedTreeViewItem != null)
{
if (e.NewValue == m_selectedTreeViewItem)
{
// Will only end up here when reversing item
// Without this line childs can't be selected
// twice if "No" was pressed in the question..
c_treeView.Focus();
}
else
{
if (MessageBox.Show("Change TreeViewItem?",
"Really change",
MessageBoxButton.YesNo,
MessageBoxImage.Question) != MessageBoxResult.Yes)
{
EventHandler eventHandler = null;
eventHandler = new EventHandler(delegate
{
c_treeView.LayoutUpdated -= eventHandler;
m_selectedTreeViewItem.IsSelected = true;
});
// Will be fired after SelectedItemChanged, to early to change back here
c_treeView.LayoutUpdated += eventHandler;
}
else
{
m_selectedTreeViewItem = e.NewValue as MyInterface;
}
}
}
else
{
m_selectedTreeViewItem = e.NewValue as MyInterface;
}
}
I haven't found any situation where it doesn't revert back to the previous item upon pressing "No".
I had to solve the same problem, but in multiple treeviews in my application. I derived TreeView and added event handlers, partly using Meleak's solution and partly using the extension methods from this forum: http://forums.silverlight.net/t/65277.aspx/1/10
I thought I'd share my solution with you, so here is my complete reusable TreeView that handles "cancel node change":
public class MyTreeView : TreeView
{
public static RoutedEvent PreviewSelectedItemChangedEvent;
public static RoutedEvent SelectionCancelledEvent;
static MyTreeView()
{
PreviewSelectedItemChangedEvent = EventManager.RegisterRoutedEvent("PreviewSelectedItemChanged", RoutingStrategy.Bubble,
typeof(RoutedPropertyChangedEventHandler<object>), typeof(MyTreeView));
SelectionCancelledEvent = EventManager.RegisterRoutedEvent("SelectionCancelled", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(MyTreeView));
}
public event RoutedPropertyChangedEventHandler<object> PreviewSelectedItemChanged
{
add { AddHandler(PreviewSelectedItemChangedEvent, value); }
remove { RemoveHandler(PreviewSelectedItemChangedEvent, value); }
}
public event RoutedEventHandler SelectionCancelled
{
add { AddHandler(SelectionCancelledEvent, value); }
remove { RemoveHandler(SelectionCancelledEvent, value); }
}
private object selectedItem = null;
protected override void OnSelectedItemChanged(RoutedPropertyChangedEventArgs<object> e)
{
if (e.NewValue == selectedItem)
{
this.Focus();
var args = new RoutedEventArgs(SelectionCancelledEvent);
RaiseEvent(args);
}
else
{
var args = new RoutedPropertyChangedEventArgs<object>(e.OldValue, e.NewValue, PreviewSelectedItemChangedEvent);
RaiseEvent(args);
if (args.Handled)
{
EventHandler eventHandler = null;
eventHandler = delegate
{
this.LayoutUpdated -= eventHandler;
var treeViewItem = this.ContainerFromItem(selectedItem);
if (treeViewItem != null)
treeViewItem.IsSelected = true;
};
this.LayoutUpdated += eventHandler;
}
else
{
selectedItem = this.SelectedItem;
base.OnSelectedItemChanged(e);
}
}
}
}
public static class TreeViewExtensions
{
public static TreeViewItem ContainerFromItem(this TreeView treeView, object item)
{
if (item == null) return null;
var containerThatMightContainItem = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(item);
return containerThatMightContainItem ?? ContainerFromItem(treeView.ItemContainerGenerator, treeView.Items, item);
}
private static TreeViewItem ContainerFromItem(ItemContainerGenerator parentItemContainerGenerator, ItemCollection itemCollection, object item)
{
foreach (var child in itemCollection)
{
var parentContainer = (TreeViewItem)parentItemContainerGenerator.ContainerFromItem(child);
var containerThatMightContainItem = (TreeViewItem)parentContainer.ItemContainerGenerator.ContainerFromItem(item);
if (containerThatMightContainItem != null)
return containerThatMightContainItem;
var recursionResult = ContainerFromItem(parentContainer.ItemContainerGenerator, parentContainer.Items, item);
if (recursionResult != null)
return recursionResult;
}
return null;
}
}
Here is an example of usage (codebehind for window containing a MyTreeView):
private void theTreeView_PreviewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (e.OldValue != null)
e.Handled = true;
}
private void theTreeView_SelectionCancelled(object sender, RoutedEventArgs e)
{
MessageBox.Show("Cancelled");
}
After choosing the first node in the treeview, all other node changes are cancelled and a message box is displayed.
You can't actually put your logic into the OnSelectedItemChanged Method, if the logic is there the Selected Item has actually already changed.
As suggested by another poster, the PreviewMouseDown handler is a better spot to implement the logic, however, a fair amount of leg work still needs to be done.
Below is my 2 cents:
First the TreeView that I have implemented:
public class MyTreeView : TreeView
{
static MyTreeView( )
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(MyTreeView),
new FrameworkPropertyMetadata(typeof(TreeView)));
}
// Register a routed event, note this event uses RoutingStrategy.Tunnel. per msdn docs
// all "Preview" events should use tunneling.
// http://msdn.microsoft.com/en-us/library/system.windows.routedevent.routingstrategy.aspx
public static RoutedEvent PreviewSelectedItemChangedEvent = EventManager.RegisterRoutedEvent(
"PreviewSelectedItemChanged",
RoutingStrategy.Tunnel,
typeof(CancelEventHandler),
typeof(MyTreeView));
// give CLR access to routed event
public event CancelEventHandler PreviewSelectedItemChanged
{
add
{
AddHandler(PreviewSelectedItemChangedEvent, value);
}
remove
{
RemoveHandler(PreviewSelectedItemChangedEvent, value);
}
}
// override PreviewMouseDown
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
// determine which item is going to be selected based on the current mouse position
object itemToBeSelected = this.GetObjectAtPoint<TreeViewItem>(e.GetPosition(this));
// selection doesn't change if the target point is null (beyond the end of the list)
// or if the item to be selected is already selected.
if (itemToBeSelected != null && itemToBeSelected != SelectedItem)
{
bool shouldCancel;
// call our new event
OnPreviewSelectedItemChanged(out shouldCancel);
if (shouldCancel)
{
// if we are canceling the selection, mark this event has handled and don't
// propogate the event.
e.Handled = true;
return;
}
}
// otherwise we want to continue normally
base.OnPreviewMouseDown(e);
}
protected virtual void OnPreviewSelectedItemChanged(out bool shouldCancel)
{
CancelEventArgs e = new CancelEventArgs( );
if (PreviewSelectedItemChangedEvent != null)
{
// Raise our event with our custom CancelRoutedEventArgs
RaiseEvent(new CancelRoutedEventArgs(PreviewSelectedItemChangedEvent, e));
}
shouldCancel = e.Cancel;
}
}
some extension methods to support the TreeView finding the object under the mouse.
public static class ItemContainerExtensions
{
// get the object that exists in the container at the specified point.
public static object GetObjectAtPoint<ItemContainer>(this ItemsControl control, Point p)
where ItemContainer : DependencyObject
{
// ItemContainer - can be ListViewItem, or TreeViewItem and so on(depends on control)
ItemContainer obj = GetContainerAtPoint<ItemContainer>(control, p);
if (obj == null)
return null;
// it is worth noting that the passed _control_ may not be the direct parent of the
// container that exists at this point. This can be the case in a TreeView, where the
// parent of a TreeViewItem may be either the TreeView or a intermediate TreeViewItem
ItemsControl parentGenerator = obj.GetParentItemsControl( );
// hopefully this isn't possible?
if (parentGenerator == null)
return null;
return parentGenerator.ItemContainerGenerator.ItemFromContainer(obj);
}
// use the VisualTreeHelper to find the container at the specified point.
public static ItemContainer GetContainerAtPoint<ItemContainer>(this ItemsControl control, Point p)
where ItemContainer : DependencyObject
{
HitTestResult result = VisualTreeHelper.HitTest(control, p);
DependencyObject obj = result.VisualHit;
while (VisualTreeHelper.GetParent(obj) != null && !(obj is ItemContainer))
{
obj = VisualTreeHelper.GetParent(obj);
}
// Will return null if not found
return obj as ItemContainer;
}
// walk up the visual tree looking for the nearest ItemsControl parent of the specified
// depObject, returns null if one isn't found.
public static ItemsControl GetParentItemsControl(this DependencyObject depObject)
{
DependencyObject obj = VisualTreeHelper.GetParent(depObject);
while (VisualTreeHelper.GetParent(obj) != null && !(obj is ItemsControl))
{
obj = VisualTreeHelper.GetParent(obj);
}
// will return null if not found
return obj as ItemsControl;
}
}
and last, but not least the custom EventArgs that leverage the RoutedEvent subsystem.
public class CancelRoutedEventArgs : RoutedEventArgs
{
private readonly CancelEventArgs _CancelArgs;
public CancelRoutedEventArgs(RoutedEvent #event, CancelEventArgs cancelArgs)
: base(#event)
{
_CancelArgs = cancelArgs;
}
// override the InvokeEventHandler because we are going to pass it CancelEventArgs
// not the normal RoutedEventArgs
protected override void InvokeEventHandler(Delegate genericHandler, object genericTarget)
{
CancelEventHandler handler = (CancelEventHandler)genericHandler;
handler(genericTarget, _CancelArgs);
}
// the result
public bool Cancel
{
get
{
return _CancelArgs.Cancel;
}
}
}
Instead of selecting for Selected/Unselected, a better route might be to hook into PreviewMouseDown. The preblem with handling a Selected and Unselected event is that the event has already occurred when you receive the notification. There is nothing to cancel because it's already happened.
On the other hand, Preview events are cancelable. It's not the exact event you want but it does give you the oppuritunity to prevent the user from selecting a different node.
You can't cancel the event like you can, for example, a Closing event. But you can undo it if you cache the last selected value. The secret is you have to change the selection without re-firing the SelectionChanged event. Here's an example:
private object _LastSelection = null;
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (IsUpdated)
{
MessageBoxResult result = MessageBox.Show("The current record has been modified. Are you sure you want to navigate away? Click Cancel to continue editing. If you click OK all changes will be lost.", "Warning", MessageBoxButton.OKCancel, MessageBoxImage.Hand);
switch (result)
{
case MessageBoxResult.Cancel:
e.Handled = true;
// disable event so this doesn't go into an infinite loop when the selection is changed to the cached value
PersonListView.SelectionChanged -= new SelectionChangedEventHandler(OnSelectionChanged);
PersonListView.SelectedItem = _LastSelection;
PersonListView.SelectionChanged += new SelectionChangedEventHandler(OnSelectionChanged);
return;
case MessageBoxResult.OK:
// revert the object to the original state
LocalDataContext.Persons.GetOriginalEntityState(_LastSelection).CopyTo(_LastSelection);
IsUpdated = false;
Refresh();
break;
default:
throw new ApplicationException("Invalid response.");
}
}
// cache the selected item for undo
_LastSelection = PersonListView.SelectedItem;
}
CAMS_ARIES:
XAML:
code :
private bool ManejarSeleccionNodoArbol(Object origen)
{
return true; // with true, the selected nodo don't change
return false // with false, the selected nodo change
}
private void Arbol_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.Source is TreeViewItem)
{
e.Handled = ManejarSeleccionNodoArbol(e.Source);
}
}
private void Arbol_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Source is TreeViewItem)
{
e.Handled=ManejarSeleccionNodoArbol(e.Source);
}
}
Since the SelectedItemChanged event is triggered after the SelectedItem has already changed, you can't really cancel the event at this point.
What you can do is listen for mouse-clicks and cancel them before the SelectedItem gets changed.
You could create your custom control that derives from TreeView and then override the OnSelectedItemChanged method.
Before calling the base, you could first fire a custom event with a CancelEventArgs parameter. If the parameter.Cancel become true, then don't call the base, but select the old item instead (be careful that the OnSelectedItemChanged will be called again).
Not the best solution, but at least this keeps the logic inside the tree control, and there is not chance that the selection change event fires more than it's needed. Also, you don't need to care if the user clicked the tree, used the keyboard or maybe the selection changed programatically.
I solved this problem for 1 tree view and display of 1 document at a time. This solution is based on an attachable behavior that can be attached to a normal treeview:
<TreeView Grid.Column="0"
ItemsSource="{Binding TreeViewItems}"
behav:TreeViewSelectionChangedBehavior.ChangedCommand="{Binding SelectItemChangedCommand}"
>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"
ToolTipService.ShowOnDisabled="True"
VerticalAlignment="Center" Margin="3" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
and the code for the behavior is this:
/// <summary>
/// Source:
/// http://stackoverflow.com/questions/1034374/drag-and-drop-in-mvvm-with-scatterview
/// http://social.msdn.microsoft.com/Forums/de-DE/wpf/thread/21bed380-c485-44fb-8741-f9245524d0ae
///
/// Attached behaviour to implement the SelectionChanged command/event via delegate command binding or routed commands.
/// </summary>
public static class TreeViewSelectionChangedBehavior
{
#region fields
/// <summary>
/// Field of attached ICommand property
/// </summary>
private static readonly DependencyProperty ChangedCommandProperty = DependencyProperty.RegisterAttached(
"ChangedCommand",
typeof(ICommand),
typeof(TreeViewSelectionChangedBehavior),
new PropertyMetadata(null, OnSelectionChangedCommandChange));
/// <summary>
/// Implement backing store for UndoSelection dependency proeprty to indicate whether selection should be
/// cancelled via MessageBox query or not.
/// </summary>
public static readonly DependencyProperty UndoSelectionProperty =
DependencyProperty.RegisterAttached("UndoSelection",
typeof(bool),
typeof(TreeViewSelectionChangedBehavior),
new PropertyMetadata(false, OnUndoSelectionChanged));
#endregion fields
#region methods
#region ICommand changed methods
/// <summary>
/// Setter method of the attached ChangedCommand <seealso cref="ICommand"/> property
/// </summary>
/// <param name="source"></param>
/// <param name="value"></param>
public static void SetChangedCommand(DependencyObject source, ICommand value)
{
source.SetValue(ChangedCommandProperty, value);
}
/// <summary>
/// Getter method of the attached ChangedCommand <seealso cref="ICommand"/> property
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static ICommand GetChangedCommand(DependencyObject source)
{
return (ICommand)source.GetValue(ChangedCommandProperty);
}
#endregion ICommand changed methods
#region UndoSelection methods
public static bool GetUndoSelection(DependencyObject obj)
{
return (bool)obj.GetValue(UndoSelectionProperty);
}
public static void SetUndoSelection(DependencyObject obj, bool value)
{
obj.SetValue(UndoSelectionProperty, value);
}
#endregion UndoSelection methods
/// <summary>
/// This method is hooked in the definition of the <seealso cref="ChangedCommandProperty"/>.
/// It is called whenever the attached property changes - in our case the event of binding
/// and unbinding the property to a sink is what we are looking for.
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnSelectionChangedCommandChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TreeView uiElement = d as TreeView; // Remove the handler if it exist to avoid memory leaks
if (uiElement != null)
{
uiElement.SelectedItemChanged -= Selection_Changed;
var command = e.NewValue as ICommand;
if (command != null)
{
// the property is attached so we attach the Drop event handler
uiElement.SelectedItemChanged += Selection_Changed;
}
}
}
/// <summary>
/// This method is called when the selection changed event occurs. The sender should be the control
/// on which this behaviour is attached - so we convert the sender into a <seealso cref="UIElement"/>
/// and receive the Command through the <seealso cref="GetChangedCommand"/> getter listed above.
///
/// The <paramref name="e"/> parameter contains the standard EventArgs data,
/// which is unpacked and reales upon the bound command.
///
/// This implementation supports binding of delegate commands and routed commands.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void Selection_Changed(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var uiElement = sender as TreeView;
// Sanity check just in case this was somehow send by something else
if (uiElement == null)
return;
ICommand changedCommand = TreeViewSelectionChangedBehavior.GetChangedCommand(uiElement);
// There may not be a command bound to this after all
if (changedCommand == null)
return;
// Check whether this attached behaviour is bound to a RoutedCommand
if (changedCommand is RoutedCommand)
{
// Execute the routed command
(changedCommand as RoutedCommand).Execute(e.NewValue, uiElement);
}
else
{
// Execute the Command as bound delegate
changedCommand.Execute(e.NewValue);
}
}
/// <summary>
/// Executes when the bound boolean property indicates that a user should be asked
/// about changing a treeviewitem selection instead of just performing it.
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnUndoSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TreeView uiElement = d as TreeView; // Remove the handler if it exist to avoid memory leaks
if (uiElement != null)
{
uiElement.PreviewMouseDown -= uiElement_PreviewMouseDown;
var command = (bool)e.NewValue;
if (command == true)
{
// the property is attached so we attach the Drop event handler
uiElement.PreviewMouseDown += uiElement_PreviewMouseDown;
}
}
}
/// <summary>
/// Based on the solution proposed here:
/// Source: http://stackoverflow.com/questions/20244916/wpf-treeview-selection-change
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void uiElement_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
// first did the user click on a tree node?
var source = e.OriginalSource as DependencyObject;
while (source != null && !(source is TreeViewItem))
source = VisualTreeHelper.GetParent(source);
var itemSource = source as TreeViewItem;
if (itemSource == null)
return;
var treeView = sender as TreeView;
if (treeView == null)
return;
bool undoSelection = TreeViewSelectionChangedBehavior.GetUndoSelection(treeView);
if (undoSelection == false)
return;
// Cancel the attempt to select an item.
var result = MessageBox.Show("The current document has unsaved data. Do you want to continue without saving data?", "Are you really sure?",
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
if (result == MessageBoxResult.No)
{
// Cancel the attempt to select a differnet item.
e.Handled = true;
}
else
{
// Lets disable this for a moment, otherwise, we'll get into an event "recursion"
treeView.PreviewMouseDown -= uiElement_PreviewMouseDown;
// Select the new item - make sure a SelectedItemChanged event is fired in any case
// Even if this means that we have to deselect/select the one and the same item
if (itemSource.IsSelected == true )
itemSource.IsSelected = false;
itemSource.IsSelected = true;
// Lets enable this to get back to business for next selection
treeView.PreviewMouseDown += uiElement_PreviewMouseDown;
}
}
#endregion methods
}
In this example I am showing a blocking message box in order to block the PreviewMouseDown event when it occurs. The event is then handled to signal that selection is cancelled or it is not handled to let the treeview itself handle the event by selecting the item that is about to be selected.
The behavior then invokes a bound command in the viewmodel if the user decides to continue anyway (PreviewMouseDown event is not handled by attached behavior and bound command is invoked.
I guess the message box showing could be done in other ways but I think its essential here to block the event when it happens since its otherwise not possible to cancel it(?). So, the only improve I could possible think off about this code is to bind some strings to make the displayed message configurable.
I have written an article that contains a downloadable sample since this is otherwise a difficult area to explain (one has to make many assumptions about missing parts that and the may not always be shared by all readers)
Here is an article that contains my results:
http://www.codeproject.com/Articles/995629/Cancelable-TreeView-Navigation-for-Documents-in-WP
Please comment on this solution and let me know if you see room for improvement.
Related
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.
I've got a strange problem here regarding sorting of a WPF DataGrid (System.Windows.Controls.DataGrid in .NET 4.0).
Its ItemsSource is bound to a property of the datacontext object:
<DataGrid HeadersVisibility="Column" SelectedIndex="0" MinHeight="30" ItemsSource="{Binding FahrtenView}" AutoGenerateColumns="False" x:Name="fahrtenDG">
FahrtenView looks like this:
public ICollectionView FahrtenView
{
get
{
var view = CollectionViewSource.GetDefaultView(_fahrten);
view.SortDescriptions.Add(new SortDescription("Index", ListSortDirection.Ascending));
return view;
}
}
The DataGrid gets sorted. However it only gets sorted the very first time it's assigned a DataContext. After that, changing the DataContext (by selecting another "parental" object in a data hierarchy) still causes the property FahrtenView to be evaluated (I can put a BP in and debugger stops there) but the added sortdescription is completely ignored, hence sorting does not work anymore.
Even calling fahrtenDG.Items.Refresh() on every DataContextChange doesn't help.
I'm pretty sure this is the way to go when it comes to sorting a WPF DataGrid, isn't it? So why does it refuse to work so obstinately after doing its job perfectly the very first time it gets called?
Any idea? I'd be very grateful.
Cheers,
Hendrik
I've inherited from DataGrid to catch a brief glimpse on its guts. What I've found is that for some mysterious reasons, although the first time OnItemsSourceChanged gets called, everything looks fine, in every following call of OnItemsSourceChanged the SortDescription list of the ItemsSource collection view is empty.
For that reason I've added a custom SetupSortDescription event that is called at the end of OnItemsSourceChanged. Now I'm adding the sort descriptions in the event handler function, which's working like a charm.
I consider this a bug in the WPF toolkit DataGrid.
Here's my overridden OnItemsSourceChanged
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (SetupSortDescriptions != null && (newValue != null))
SetupSortDescriptions(this, new ValueEventArgs<CollectionView>((CollectionView)newValue));
base.OnItemsSourceChanged(oldValue, newValue);
}
I improved on Hendrik's answer a bit to use MVVM rather than an event.
public static readonly DependencyProperty SortDescriptionsProperty = DependencyProperty.Register("SortDescriptions", typeof(List<SortDescription>), typeof(ReSolverDataGrid), new PropertyMetadata(null));
/// <summary>
/// Sort descriptions for when grouped LCV is being used. Due to bu*g in WCF this must be set otherwise sort is ignored.
/// </summary>
/// <remarks>
/// IN YOUR XAML, THE ORDER OF BINDINGS IS IMPORTANT! MAKE SURE SortDescriptions IS SET BEFORE ITEMSSOURCE!!!
/// </remarks>
public List<SortDescription> SortDescriptions
{
get { return (List<SortDescription>)GetValue(SortDescriptionsProperty); }
set { SetValue(SortDescriptionsProperty, value); }
}
protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
{
//Only do this if the newValue is a listcollectionview - in which case we need to have it re-populated with sort descriptions due to DG bug
if (SortDescriptions != null && ((newValue as ListCollectionView) != null))
{
var listCollectionView = (ListCollectionView)newValue;
listCollectionView.SortDescriptions.AddRange(SortDescriptions);
}
base.OnItemsSourceChanged(oldValue, newValue);
}
I used the interited DataGrid from kat to create a Behavior for the WPF DataGrid.
The behavior saves the initial SortDescriptions and applies them on every change of ItemsSource.
You can also provide a IEnumerable<SortDescription> which will cause a resort on every change.
Behavior
public class DataGridSortBehavior : Behavior<DataGrid>
{
public static readonly DependencyProperty SortDescriptionsProperty = DependencyProperty.Register(
"SortDescriptions",
typeof (IEnumerable<SortDescription>),
typeof (DataGridSortBehavior),
new FrameworkPropertyMetadata(null, SortDescriptionsPropertyChanged));
/// <summary>
/// Storage for initial SortDescriptions
/// </summary>
private IEnumerable<SortDescription> _internalSortDescriptions;
/// <summary>
/// Property for providing a Binding to Custom SortDescriptions
/// </summary>
public IEnumerable<SortDescription> SortDescriptions
{
get { return (IEnumerable<SortDescription>) GetValue(SortDescriptionsProperty); }
set { SetValue(SortDescriptionsProperty, value); }
}
protected override void OnAttached()
{
var dpd = DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof (DataGrid));
if (dpd != null)
{
dpd.AddValueChanged(AssociatedObject, OnItemsSourceChanged);
}
}
protected override void OnDetaching()
{
var dpd = DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof (DataGrid));
if (dpd != null)
{
dpd.RemoveValueChanged(AssociatedObject, OnItemsSourceChanged);
}
}
private static void SortDescriptionsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DataGridSortBehavior)
{
((DataGridSortBehavior) d).OnItemsSourceChanged(d, EventArgs.Empty);
}
}
public void OnItemsSourceChanged(object sender, EventArgs eventArgs)
{
// save description only on first call, SortDescriptions are always empty after ItemsSourceChanged
if (_internalSortDescriptions == null)
{
// save initial sort descriptions
var cv = (AssociatedObject.ItemsSource as ICollectionView);
if (cv != null)
{
_internalSortDescriptions = cv.SortDescriptions.ToList();
}
}
else
{
// do not resort first time - DataGrid works as expected this time
var sort = SortDescriptions ?? _internalSortDescriptions;
if (sort != null)
{
sort = sort.ToList();
var collectionView = AssociatedObject.ItemsSource as ICollectionView;
if (collectionView != null)
{
using (collectionView.DeferRefresh())
{
collectionView.SortDescriptions.Clear();
foreach (var sorter in sort)
{
collectionView.SortDescriptions.Add(sorter);
}
}
}
}
}
}
}
XAML with optional SortDescriptions parameter
<DataGrid ItemsSource="{Binding View}" >
<i:Interaction.Behaviors>
<commons:DataGridSortBehavior SortDescriptions="{Binding SortDescriptions}"/>
</i:Interaction.Behaviors>
</DataGrid>
ViewModel ICollectionView Setup
View = CollectionViewSource.GetDefaultView(_collection);
View.SortDescriptions.Add(new SortDescription("Sequence", ListSortDirection.Ascending));
Optional: ViewModel Property for providing changable SortDescriptions
public IEnumerable<SortDescription> SortDescriptions
{
get
{
return new List<SortDescription> {new SortDescription("Sequence", ListSortDirection.Ascending)};
}
}
If you call CollectionViewSource.GetDefaultView(..) on the same collection you get the same collectionview object back, that could explain why adding an identical sortdescription struct doesn't trigger a change.
fahrtenDG.Items.Refresh() can't work since you are not refreshing the bound collection.
CollectionViewSource.GetDefaultView(_fahrten).Refresh() should work - I would keep a reference to it.
From your explanation I don't quite get the change of datacontext - are you changing it to a new object? If so all your bindings should reevaluate. Is it the same collection always, and your Index property on the listelements change, and that is why you expect a change - if so your list element might need a INotifyPropertyChanged implementation, because if the collection doesn't change then there is no need to resort.
Your OnItemsSourceChanged(..) implementation seems like a hack :)
I tried to get around this problem with the view model - by recreating ICollectionView in the getter and frantically calling DeferRefresh(). However I can confirm that Hendrik's solution is the only one that works reliably. I wanted to post full code below in case it helps somebody.
VIEW
<controls:SortableDataGrid
ItemsSource="{Binding InfoSorted}"
PermanentSort="{Binding PermanentSort}"
CanUserSortColumns="False" />
VIEW MODEL
public ObservableCollection<Foo> Info { get; private set; }
public ICollectionView InfoSorted { get; private set; }
public IEnumerable<SortDescription> PermanentSort { get; private set; }
CUSTOM CONTROL
public class SortableDataGrid : DataGrid
{
public static readonly DependencyProperty PermanentSortProperty = DependencyProperty.Register(
"PermanentSort",
typeof(IEnumerable<SortDescription>),
typeof(SortableDataGrid),
new FrameworkPropertyMetadata(null));
public IEnumerable<SortDescription> PermanentSort
{
get { return (IEnumerable<SortDescription>)this.GetValue(PermanentSortProperty); }
set { this.SetValue(PermanentSortProperty, value); }
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
var sort = this.PermanentSort;
if (sort != null)
{
sort = sort.ToList();
var collectionView = newValue as ICollectionView;
if (collectionView != null)
{
using (collectionView.DeferRefresh())
{
collectionView.SortDescriptions.Clear();
foreach (SortDescription sorter in sort)
{
collectionView.SortDescriptions.Add(sorter);
}
}
}
}
base.OnItemsSourceChanged(oldValue, newValue);
}
}
I endorse Juergen's approach of using an attached behavior. However, since my version of this problem arose when I had declared the CollectionViewSource object in the view model class, I found it more direct to solve the problem by adding the event handler SortDescriptions_CollectionChanged as shown in the code below. This code is entirely within the view model class.
public CollectionViewSource FilteredOptionsView
{
get
{
if (_filteredOptionsView == null)
{
_filteredOptionsView = new CollectionViewSource
{
Source = Options,
IsLiveSortingRequested = true
};
SetOptionsViewSorting(_filteredOptionsView);
_filteredOptionsView.View.Filter = o => ((ConstantOption)o).Value != null;
}
return _filteredOptionsView;
}
}
private CollectionViewSource _filteredOptionsView;
protected void SetOptionsViewSorting(CollectionViewSource viewSource)
{
// define the sorting
viewSource.SortDescriptions.Add(_optionsViewSortDescription);
// subscribe to an event in order to handle a bug caused by the DataGrid that may be
// bound to the CollectionViewSource
((INotifyCollectionChanged)viewSource.View.SortDescriptions).CollectionChanged
+= SortDescriptions_CollectionChanged;
}
protected static SortDescription _optionsViewSortDescription
= new SortDescription("SortIndex", ListSortDirection.Ascending);
void SortDescriptions_CollectionChanged(Object sender, NotifyCollectionChangedEventArgs e)
{
var collection = sender as SortDescriptionCollection;
if (collection == null) return;
// The SortDescriptions collection should always contain exactly one SortDescription.
// However, when DataTemplate containing the DataGrid bound to the ICollectionView
// is unloaded, the DataGrid erroneously clears the collection.
if (collection.None())
collection.Add(_optionsViewSortDescription);
}
Thanks! This was driving me batty! I modified your code to suite my needs. It basically persists the sort descriptions and restores them whenever they get blown away. This may help others:
private List<SortDescription> SortDescriptions = null;
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue is CollectionView collectionView)
if (SortDescriptions == null)
SortDescriptions = new List<SortDescription>(collectionView.SortDescriptions);
else
foreach (SortDescription sortDescription in SortDescriptions)
collectionView.SortDescriptions.Add(sortDescription);
base.OnItemsSourceChanged(oldValue, newValue);
}
In a Silverlight MVVMLight 4.0 application I have a listbox, a textbox and a checkbox.
The listbox's ItemsSource is bound to a list of objects in the viewmodel.
The listbox's SelectedItem is two-way bound to an object (SelectedActivity) in the viewmodel.
Both the textbox's Text and the checkbox's IsSelected properties are two-way bound to the SelectedActivity object (Name and Selected properties) in the viewmodel.
There is no codebehind.
This works fine: changing the Name in the textbox or checking/unchecking the checkbox and then tabbing will change the underlying property of the object.
But when I change the name (or the checked state) and then immediatelly click another item in the list, the change is not registered.
Does anybody have a workaround for this?
kind regards,
Karel
This is the XAML:
<ListBox Height="251" HorizontalAlignment="Left" Margin="11,39,0,0" Name="activitiesListBox" ItemsSource="{Binding Activities.Items}" VerticalAlignment="Top" Width="139"
SelectedItem="{Binding Activities.SelectedActivity, Mode=TwoWay}">
This is the Activities class holding the items bound to the list:
public class CLJActivitiesViewModel : ViewModelBase
{
/// <summary>
/// Initializes a new instance of the ActivitiesViewModel class.
/// </summary>
public CLJActivitiesViewModel()
{
////if (IsInDesignMode)
////{
//// // Code runs in Blend --> create design time data.
////}
////else
////{
//// // Code runs "for real": Connect to service, etc...
////}
}
#region items
/// <summary>
/// The <see cref="Items" /> property's name.
/// </summary>
public const string ItemsPropertyName = "Items";
private ObservableCollection<CLJActivityViewModel> m_Items = null;
/// <summary>
/// Gets the Items property.
/// TODO Update documentation:
/// Changes to that property's value raise the PropertyChanged event.
/// This property's value is broadcasted by the Messenger's default instance when it changes.
/// </summary>
public ObservableCollection<CLJActivityViewModel> Items
{
get
{
return m_Items;
}
set
{
if (m_Items == value)
{
return;
}
var oldValue = m_Items;
m_Items = value;
RaisePropertyChanged(ItemsPropertyName, oldValue, value, true);
}
}
#endregion
#region SelectedActivity
/// <summary>
/// The <see cref="SelectedActivity" /> property's name.
/// </summary>
public const string SelectedActivityPropertyName = "SelectedActivity";
private CLJActivityViewModel m_SelectedActivity = null;
/// <summary>
/// Gets the SelectedActivity property.
/// TODO Update documentation:
/// Changes to that property's value raise the PropertyChanged event.
/// This property's value is broadcasted by the Messenger's default instance when it changes.
/// </summary>
public CLJActivityViewModel SelectedActivity
{
get
{
return m_SelectedActivity;
}
set
{
if (m_SelectedActivity == value)
{
return;
}
var oldValue = m_SelectedActivity;
m_SelectedActivity = value;
RaisePropertyChanged(SelectedActivityPropertyName, oldValue, value, true);
}
}
#endregion
public override void Cleanup()
{
// Clean own resources if needed
base.Cleanup();
}
}
I ran into the kinda the same issue. I had to trigger the update as the user was entering text so that I could do some validation.
An easy way to achieve that is to create a custom behaviour that you can then add to any TextBox.
Mine is as follows:
public static class TextChangedBindingBehavior
{
public static readonly DependencyProperty InstanceProperty =
DependencyProperty.RegisterAttached("Instance", typeof(object), typeof(TextChangedBindingBehavior), new PropertyMetadata(OnSetInstanceCallback));
public static object GetInstance(DependencyObject obj)
{
return (object)obj.GetValue(InstanceProperty);
}
public static void SetInstance(DependencyObject obj, object value)
{
obj.SetValue(InstanceProperty, value);
}
private static void OnSetInstanceCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textBox = d as TextBox;
if (textBox != null)
{
textBox.TextChanged -= OnTextChanged;
textBox.TextChanged += OnTextChanged;
}
}
private static void OnTextChanged(object sender, TextChangedEventArgs e)
{
var textBox = (TextBox)sender;
if(!DesignerProperties.GetIsInDesignMode(textBox))
{
textBox.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
}
}
and you set it to the TextBox like that (Behaviors is the namespace where I put the class above):
<TextBox Behaviors:TextChangedBindingBehavior.Instance="" Text="{Binding Name, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" />
I've ran into the issue like that with TextBox, but didn't see it affecting check box. TextBox issue is happening because bound text gets updated then focus is lost. That is why if you tab first and then change your selection it works as you expect. If you change selection directly bound text doesn't get updated since focus lost message arrives too late.
One way of dealing with this issue is to force binding update every time user types text in the text box. You can make custom behaviour to keep it mvvm.
In wpf I setup a tab control that binds to a collection of objects each object has a data template with a data grid presenting the data. If I select a particular cell and put it into edit mode, leaving the grid by going to another tab this will cause the exception below to be thrown on returning the datagrid:
'DeferRefresh' is not allowed during an AddNew or EditItem transaction.
It appears that the cell never left edit mode. Is there an easy way to take the cell out of edit mode, or is something else going on here?
Update: It looks like if I do not bind the tab control to the data source, but instead explicitly define each tab and then bind each item in the data source to a content control this problem goes away. This is not really a great solution, so I would still like to know how to bind the collection directly to the tab control.
Update: So what I have actually done for my own solution is to use a ListView and a content control in place of a tab control. I use a style to make the list view look tab like. The view model exposes a set of child view models and allows the user to select one via the list view. The content control then presents the selected view model and each view model has an associated data template which contains the data grid. With this setup switching between view models while in edit mode on the grid will properly end edit mode and save the data.
Here is the xaml for setting this up:
<ListView ItemTemplate="{StaticResource MakeItemsLookLikeTabs}"
ItemsSource="{Binding ViewModels}"
SelectedItem="{Binding Selected}"
Style="{StaticResource MakeItLookLikeATabControl}"/>
<ContentControl Content="{Binding Selected}">
I'll accept Phil's answer as that should work also, but for me the solution above seems like it will be more portable between projects.
I implemented a behavior for the DataGrid based on code I found in this thread.
Usage:<DataGrid local:DataGridCommitEditBehavior.CommitOnLostFocus="True" />
Code:
using System.Collections.Generic;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
/// <summary>
/// Provides an ugly hack to prevent a bug in the data grid.
/// https://connect.microsoft.com/VisualStudio/feedback/details/532494/wpf-datagrid-and-tabcontrol-deferrefresh-exception
/// </summary>
public class DataGridCommitEditBehavior
{
public static readonly DependencyProperty CommitOnLostFocusProperty =
DependencyProperty.RegisterAttached(
"CommitOnLostFocus",
typeof(bool),
typeof(DataGridCommitEditBehavior),
new UIPropertyMetadata(false, OnCommitOnLostFocusChanged));
/// <summary>
/// A hack to find the data grid in the event handler of the tab control.
/// </summary>
private static readonly Dictionary<TabPanel, DataGrid> ControlMap = new Dictionary<TabPanel, DataGrid>();
public static bool GetCommitOnLostFocus(DataGrid datagrid)
{
return (bool)datagrid.GetValue(CommitOnLostFocusProperty);
}
public static void SetCommitOnLostFocus(DataGrid datagrid, bool value)
{
datagrid.SetValue(CommitOnLostFocusProperty, value);
}
private static void CommitEdit(DataGrid dataGrid)
{
dataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
dataGrid.CommitEdit(DataGridEditingUnit.Row, true);
}
private static DataGrid GetParentDatagrid(UIElement element)
{
UIElement childElement; // element from which to start the tree navigation, looking for a Datagrid parent
if (element is ComboBoxItem)
{
// Since ComboBoxItem.Parent is null, we must pass through ItemsPresenter in order to get the parent ComboBox
var parentItemsPresenter = VisualTreeFinder.FindParentControl<ItemsPresenter>(element as ComboBoxItem);
var combobox = parentItemsPresenter.TemplatedParent as ComboBox;
childElement = combobox;
}
else
{
childElement = element;
}
var parentDatagrid = VisualTreeFinder.FindParentControl<DataGrid>(childElement);
return parentDatagrid;
}
private static TabPanel GetTabPanel(TabControl tabControl)
{
return
(TabPanel)
tabControl.GetType().InvokeMember(
"ItemsHost",
BindingFlags.NonPublic | BindingFlags.GetProperty | BindingFlags.Instance,
null,
tabControl,
null);
}
private static void OnCommitOnLostFocusChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
var dataGrid = depObj as DataGrid;
if (dataGrid == null)
{
return;
}
if (e.NewValue is bool == false)
{
return;
}
var parentTabControl = VisualTreeFinder.FindParentControl<TabControl>(dataGrid);
var tabPanel = GetTabPanel(parentTabControl);
if (tabPanel != null)
{
ControlMap[tabPanel] = dataGrid;
}
if ((bool)e.NewValue)
{
// Attach event handlers
if (parentTabControl != null)
{
tabPanel.PreviewMouseLeftButtonDown += OnParentTabControlPreviewMouseLeftButtonDown;
}
dataGrid.LostKeyboardFocus += OnDataGridLostFocus;
dataGrid.DataContextChanged += OnDataGridDataContextChanged;
dataGrid.IsVisibleChanged += OnDataGridIsVisibleChanged;
}
else
{
// Detach event handlers
if (parentTabControl != null)
{
tabPanel.PreviewMouseLeftButtonDown -= OnParentTabControlPreviewMouseLeftButtonDown;
}
dataGrid.LostKeyboardFocus -= OnDataGridLostFocus;
dataGrid.DataContextChanged -= OnDataGridDataContextChanged;
dataGrid.IsVisibleChanged -= OnDataGridIsVisibleChanged;
}
}
private static void OnDataGridDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var dataGrid = (DataGrid)sender;
CommitEdit(dataGrid);
}
private static void OnDataGridIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var senderDatagrid = (DataGrid)sender;
if ((bool)e.NewValue == false)
{
CommitEdit(senderDatagrid);
}
}
private static void OnDataGridLostFocus(object sender, KeyboardFocusChangedEventArgs e)
{
var dataGrid = (DataGrid)sender;
var focusedElement = Keyboard.FocusedElement as UIElement;
if (focusedElement == null)
{
return;
}
var focusedDatagrid = GetParentDatagrid(focusedElement);
// Let's see if the new focused element is inside a datagrid
if (focusedDatagrid == dataGrid)
{
// If the new focused element is inside the same datagrid, then we don't need to do anything;
// this happens, for instance, when we enter in edit-mode: the DataGrid element loses keyboard-focus,
// which passes to the selected DataGridCell child
return;
}
CommitEdit(dataGrid);
}
private static void OnParentTabControlPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var dataGrid = ControlMap[(TabPanel)sender];
CommitEdit(dataGrid);
}
}
public static class VisualTreeFinder
{
/// <summary>
/// Find a specific parent object type in the visual tree
/// </summary>
public static T FindParentControl<T>(DependencyObject outerDepObj) where T : DependencyObject
{
var dObj = VisualTreeHelper.GetParent(outerDepObj);
if (dObj == null)
{
return null;
}
if (dObj is T)
{
return dObj as T;
}
while ((dObj = VisualTreeHelper.GetParent(dObj)) != null)
{
if (dObj is T)
{
return dObj as T;
}
}
return null;
}
}
I have managed to work around this issue by detecting when the user clicks on a TabItem and then committing edits on visible DataGrid in the TabControl. I'm assuming the user will expect their changes to still be there when they click back.
Code snippet:
// PreviewMouseDown event handler on the TabControl
private void TabControl_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (IsUnderTabHeader(e.OriginalSource as DependencyObject))
CommitTables(yourTabControl);
}
private bool IsUnderTabHeader(DependencyObject control)
{
if (control is TabItem)
return true;
DependencyObject parent = VisualTreeHelper.GetParent(control);
if (parent == null)
return false;
return IsUnderTabHeader(parent);
}
private void CommitTables(DependencyObject control)
{
if (control is DataGrid)
{
DataGrid grid = control as DataGrid;
grid.CommitEdit(DataGridEditingUnit.Row, true);
return;
}
int childrenCount = VisualTreeHelper.GetChildrenCount(control);
for (int childIndex = 0; childIndex < childrenCount; childIndex++)
CommitTables(VisualTreeHelper.GetChild(control, childIndex));
}
This is in the code behind.
This bug is solved in the .NET Framework 4.5. You can download it at this link.
What I think you should do is pretty close to what #myermian said.
There is an event called CellEditEnding end this event would allow you to intercept and make the decision to drop the unwanted row.
private void dataGrid1_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
DataGrid grid = (DataGrid)sender;
TextBox cell = (TextBox)e.EditingElement;
if(String.IsNullOrEmpty(cell.Text) && e.EditAction == DataGridEditAction.Commit)
{
grid.CancelEdit(DataGridEditingUnit.Row);
e.Cancel = true;
}
}
Using silverlight, I have a listbox with ItemsSource bound to an ObservableCollection which is updated asynchronously. I would like to automatically select the first item in the listbox as soon as the binding is finished updating.
I can't find a good way to make this happen. I don't see any useful events to handle on the listbox and if I bind to the collection's CollectionChanged event, the binding hasn't updated yet so if I set the listbox.selectedindex at that point I get an exception that the value is out of range. Any ideas? Maybe some way to hook the binding update?
I spent a long time searching the internet to the solution for this problem, and basically ended up stumbling on the solution.
What you want to do is bind your listbox to an ICollectionView. Then make sure that you DON'T have IsSynchronizedWithCurrentItem set to false.
Bad, won't work
IsSynchronizedWithCurrentItem="False"
This is the Silverlight default, don't waste your time typing it out
IsSynchronizedWithCurrentItem="{x:Null}"
This will throw an error at runtime, and I believe is the same as {x:null} anyways
IsSynchronizedWithCurrentItem="True"
ICollectionView has a method called MoveCurrentToFirst. That name seems a bit ambiguous, but it does actually move the CurrentItem pointer to the first item (I initally thought it reordered the collection by moving whatever item you had selected to the first item position). The IsSynchronizedWithCurrentItem property allows Listbox (or any control implementing Selector) to work magic with ICollectionViews. In your code you can call ICollectioView.CurrentItem instead of whatever you bound Listbox.SelectedItem to get the currently selected item.
Here is how I make my ICollectionView available to my view (I'm using MVVM) :
public System.ComponentModel.ICollectionView NonModifierPricesView
{
get
{
if (_NonModifierPricesView == null)
{
_NonModifierPricesView = AutoRefreshCollectionViewSourceFactory.Create(x => ((MenuItemPrice)x).PriceType == MenuItemPrice.PriceTypes.NonModifier);
_NonModifierPricesView.Source = Prices;
_NonModifierPricesView.ApplyFilter(x => ((MenuItemPrice)x).DTO.Active == true);
}
ICollectionView v = _NonModifierPricesView.View;
v.MoveCurrentToFirst();
return v;
}
}
Now, as you want to bind to an observable collection, you cannot use the default CollectionViewSource as it is not aware of updates to the source collection. You probably noticed that I am using a custom CVS implementation called AutoRefreshCollectionViewSource. If memory serves, I found the code online and modified it for my own uses. I've added extra functionality for filtering, so could probably clean these classes up even more.
Here is my version of the code :
AutoRefreshCollectionViewSource.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows.Data;
public class AutoRefreshCollectionViewSource : System.Windows.Data.CollectionViewSource
{
// A delegate for launching a refresh of the view at a different priority.
private delegate void NoArgDelegate();
private Predicate<object> MyFilter; // this is the filter we put on when we do ApplyFilter
private Predicate<object> BaseFilter; // this is the filter that is applied always
public AutoRefreshCollectionViewSource(Predicate<object> _baseFilter) : base()
{
BaseFilter = _baseFilter;
if (BaseFilter == null)
BaseFilter = x => true;
}
/// <summary>
/// A collection containing all objects whose event handlers have been
/// subscribed to.
/// </summary>
private List<INotifyPropertyChanged> colSubscribedItems = new List<INotifyPropertyChanged>();
// We must override the OnSourceChanged event so that we can subscribe
// to the objects in the new collection (and unsubscribe from the old items).
protected override void OnSourceChanged(object oldSource, object newSource)
{
// Unsubscribe from the old source.
if (oldSource != null)
SubscribeSourceEvents(oldSource, true);
// Subscribe to the new source.
if (newSource != null)
SubscribeSourceEvents(newSource, false);
base.OnSourceChanged(oldSource, newSource);
}
/// <summary>
/// Adds or Removes EventHandlers to each item in the source collection as well as the
/// collection itself (if supported).
/// </summary>
/// <param name="source">The collection to (un)subscribe to and whose objects should be (un)subscribed.</param>
/// <param name="remove">Whether or not to subscribe or unsubscribe.</param>
private void SubscribeSourceEvents(object source, bool remove)
{
// Make sure the source is not nothing.
// This may occur when setting up or tearing down this object.
if (source != null)
if (source is INotifyCollectionChanged)
// We are (un)subscribing to a specialized collection, it supports the INotifyCollectionChanged event.
// (Un)subscribe to the event.
if (remove)
((INotifyCollectionChanged)source).CollectionChanged -= Handle_INotifyCollectionChanged;
else
((INotifyCollectionChanged)source).CollectionChanged += Handle_INotifyCollectionChanged;
if (remove)
// We are unsubscribing so unsubscribe from each object in the collection.
UnsubscribeAllItemEvents();
else
// We are subscribing so subscribe to each object in the collection.
SubscribeItemsEvents((IEnumerable)source, false);
}
/// <summary>
/// Unsubscribes the NotifyPropertyChanged events from all objects
/// that have been subscribed to.
/// </summary>
private void UnsubscribeAllItemEvents()
{
while (colSubscribedItems.Count > 0)
SubscribeItemEvents(colSubscribedItems[0], true);
}
/// <summary>
/// Subscribes or unsubscribes to the NotifyPropertyChanged event of all items
/// in the supplied IEnumerable.
/// </summary>
/// <param name="items">The IEnumerable containing the items to (un)subscribe to/from.</param>
/// <param name="remove">Whether or not to subscribe or unsubscribe.</param>
private void SubscribeItemsEvents(IEnumerable items, bool remove)
{
foreach (object item in items)
SubscribeItemEvents(item, remove);
}
/// <summary>
/// Subscribes or unsubscribes to the NotifyPropertyChanged event if the supplied
/// object supports it.
/// </summary>
/// <param name="item">The object to (un)subscribe to/from.</param>
/// <param name="remove">Whether or not to subscribe or unsubscribe.</param>
private void SubscribeItemEvents(object item, bool remove)
{
if (item is INotifyPropertyChanged)
// We only subscribe of the object supports INotifyPropertyChanged.
if (remove)
{
// Unsubscribe.
((INotifyPropertyChanged)item).PropertyChanged -= Item_PropertyChanged;
colSubscribedItems.Remove((INotifyPropertyChanged)item);
}
else
{
// Subscribe.
((INotifyPropertyChanged)item).PropertyChanged += Item_PropertyChanged;
colSubscribedItems.Add((INotifyPropertyChanged)item);
}
}
/// <summary>
/// Handles a property changed event from an item that supports INotifyPropertyChanged.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="e">The event arguments associated with the event.</param>
/// <remarks></remarks>
private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// By default, we do not need to refresh.
bool refresh = false;
if (e.PropertyName == "Active" || e.PropertyName == "DTO.Active")
refresh = true;
if (refresh)
// Call the refresh.
// Notice that the dispatcher will make the call to Refresh the view. If the dispatcher is not used,
// there is a possibility for a StackOverFlow to result.
this.Dispatcher.BeginInvoke(new NoArgDelegate(this.View.Refresh), null);
}
/// <summary>
/// Handles the INotifyCollectionChanged event if the subscribed source supports it.
/// </summary>
private void Handle_INotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
SubscribeItemsEvents(e.NewItems, false);
break;
case NotifyCollectionChangedAction.Remove:
SubscribeItemsEvents(e.OldItems, true);
break;
case NotifyCollectionChangedAction.Replace:
SubscribeItemsEvents(e.OldItems, true);
SubscribeItemsEvents(e.NewItems, false);
break;
case NotifyCollectionChangedAction.Reset:
UnsubscribeAllItemEvents();
SubscribeItemsEvents((IEnumerable)sender, false);
break;
}
}
public void ApplyFilter(Predicate<object> f)
{
if (f != null)
MyFilter = f;
this.View.Filter = x => MyFilter(x) && BaseFilter(x);
this.View.Refresh();
}
public void RemoveFilter()
{
this.View.Filter = BaseFilter;
this.View.Refresh();
}
}
AutoRefreshCollectionViewSourceFactory.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public class AutoRefreshCollectionViewSourceFactory
{
private static List<AutoRefreshCollectionViewSource> Collections;
public static AutoRefreshCollectionViewSource Create()
{
if (Collections == null)
Collections = new List<AutoRefreshCollectionViewSource>();
AutoRefreshCollectionViewSource cvs = new AutoRefreshCollectionViewSource(null);
Collections.Add(cvs);
return cvs;
}
public static AutoRefreshCollectionViewSource Create(Predicate<object> p)
{
if (Collections == null)
Collections = new List<AutoRefreshCollectionViewSource>();
AutoRefreshCollectionViewSource cvs = new AutoRefreshCollectionViewSource(p);
Collections.Add(cvs);
return cvs;
}
public static void ApplyFilterOnCollections()
{
foreach (AutoRefreshCollectionViewSource cvs in Collections)
cvs.ApplyFilter(null);
}
public static void RemoveFilterFromCollections()
{
foreach (AutoRefreshCollectionViewSource cvs in Collections)
cvs.RemoveFilter();
}
public static void CleanUp()
{
Collections = null;
}
}
Another thing you can do is create a UserControl which inherits from ListBox, expose a ItemsChanged by overriding the OnItemsChanged method, and handle this event and set the ListBox's SelectedIndex to 0.
public partial class MyListBox : ListBox
{
public delegate void ItemsSourceChangedHandler(object sender, EventArgs e);
#region Override
protected override void OnItemsChanged(
NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
OnItemsChangedEvent(e);
}
#endregion Override
#region Class Events
public delegate void ItemsChangedEventHandler(object sender,
NotifyCollectionChangedEventArgs e);
public event ItemsChangedEventHandler ItemsChanged;
private void OnItemsChangedEvent(
NotifyCollectionChangedEventArgs e)
{
if (ItemsChanged != null)
{
ItemsChanged(this, e);
}
}
#endregion Class Events
}
XAML for the User Control:
<ListBox x:Class="CoverArtRefiner.MyListBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
</Grid>
</ListBox>
On your screen add the following:
xmlns:local="clr-namespace:<The Namespace which MyListBox is contained in>"
Now to add the custom control onto your window/usercontrol:
<local:MyListBox ItemsChanged="listBox_ItemsChanged" Background="Black" />
And finally to handle the event:
private void listBox_ItemsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
MyListBox listBox = (MyListBox)sender;
if (listBox.Items.Count > 0)
listBox.SelectedIndex = 0;
}
On the ListBox, bind the SelectedItem property to a property on your codebind or viewmodel. Then, in the async callback handler set the property to the first item in the collection and raise the PropertyChanged event for the property(unless you already raise the event in the setter of your property):
MySelectedListItem = _entitylist.FirstOrDefault();
RasisePropertyChanged("MySelectedListItem");