I am using WPF Ribbon 4. I have a RibbonSplitButton control with dropdown menu of menu items.
When I set IsEnabled property of RibbonSplitButton to false only top button becomes disabled, not the button which opens dropdown menu.
Thanks in advance.
I solved this problem by creating my own split button, inheriting from RibbonSplitButton and adding an dependency property that I can bind to for enabling or disabling the split button alone.
public class MyRibbonSplitButton : RibbonSplitButton
{
public MyRibbonSplitButton()
: base()
{
}
/// <summary>
/// Gets or sets a value indicating whether the toggle button is enabled.
/// </summary>
/// <value><c>true</c> if the toggle button should be enabled; otherwise, <c>false</c>.</value>
public bool IsToggleButtonEnabled
{
get { return (bool)GetValue(IsToggleButtonEnabledProperty); }
set { SetValue(IsToggleButtonEnabledProperty, value); }
}
/// <summary>
/// Identifies the <see cref="IsToggleButtonEnabled"/> dependency property
/// </summary>
public static readonly DependencyProperty IsToggleButtonEnabledProperty =
DependencyProperty.Register(
"IsToggleButtonEnabled",
typeof(bool),
typeof(MyRibbonSplitButton),
new UIPropertyMetadata(true, new PropertyChangedCallback(MyRibbonSplitButton.ToggleButton_OnIsEnabledChanged)));
/// <summary>
/// Handles the PropertyChanged event for the IsToggleButtonEnabledProperty dependency property
/// </summary>
private static void ToggleButton_OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var button = sender as MyRibbonSplitButton;
var toggleButton = button.GetTemplateChild("PART_ToggleButton") as RibbonToggleButton;
toggleButton.IsEnabled = (bool)e.NewValue;
}
}
and in XAML:
<local:MyRibbonSplitButton Label="New" Command="{Binding SomeCommand}"
LargeImageSource="Images/Large/New.png"
ItemsSource="{Binding Templates}"
IsToggleButtonEnabled="{Binding HasTemplates}"/>
You can simply add a DropDownOpened="RibbonMenuButton_OnDropDownOpened" to the WPF and then
private void RibbonMenuButton_OnDropDownOpened(object sender, EventArgs e)
{
var rsb = sender as RibbonSplitButton;
if (rsb == null) return;
if (DataContext is GameCardViewModel vm)
{
rsb.IsDropDownOpen = vm.EasyInputMode;
}
}
Related
I am trying to use validation to display validation errors on windows elements (actually text boxes), but I failed to get the text boxes that are not focused/edited to update their validation when conditions for failure changed (neither using INotifyDataErrorInfo nor IDataErrorInfo).
Let's say, TextBox1 validate to Error when TextBox2 holds a specific path. Now after changing the path in TextBox2, TextBox1 should clear its error automatically, but this just did not happen, I always hat to enter the TextBox and change its content for validation to update...
Therefore I intended to use Behaviors in order to bind them to a Validation Boolean value and let the behavior set the TextBox in the appropriate VisualState using the default Validation States (Valid, InvalidFocused, InvalidUnfocused).
This is my Behavior (currently only a PoC so no Dependency Property):
/// <summary>
/// Behavior for setting the visual style depending on a validation value
/// </summary>
public class TextBoxValidationBindingBehavior : BehaviorBase<TextBox>
{
/// <summary>
/// Setup the behavior
/// </summary>
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.TextChanged += this.AssociatedObject_TextChanged;
}
/// <summary>
/// Set visual state
/// </summary>
private void AssociatedObject_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = this.AssociatedObject as TextBox;
if (textBox.Text == "Test")
{
VisualStateManager.GoToState(textBox, "Valid", false);
}
else
{
if (textBox.Focus())
{
VisualStateManager.GoToState(textBox, "InvalidFocused", true);
}
else
{
VisualStateManager.GoToState(textBox, "InvalidUnfocused", true);
}
}
}
/// <summary>
/// Clean-up the behavior
/// </summary>
protected override void OnCleanup()
{
this.AssociatedObject.TextChanged -= this.AssociatedObject_TextChanged;
base.OnCleanup();
}
}
And the TextBox definition:
<TextBox Grid.Row = "0"
Grid.Column = "1"
Margin = "0, 2, 0, 2"
VerticalAlignment = "Stretch"
VerticalContentAlignment = "Center"
Text = "{Binding NewBookName}">
<b:Interaction.Behaviors>
<behavior:TextBoxValidationBindingBehavior />
</b:Interaction.Behaviors>
</TextBox>
Setting breakpoints I can see that the code gets called as expected. But the VisualStateManager.GoToState has absolutely no impact on the TextBox!
If I define a template for the text box and set custom VisualStates the behavior will work. However, the point was not to redefine visual states for the TextBox but rather to use the existing states just by associating a Behavior bound top a validation boolean and a message to display...
I'd really appreciate any hint!!! Also, I'd be happy to provide more information if required.
For the time being, I had to give up on just setting the Visual States 😣
I needed to create a Behavior that will when bound to the control add an ad-hoc ValidationRule. In principle:
For the custom validation to work (the point is only to get the default visual style on an error), the TextChanged event needs to disable the validation, update the source, then enable validation and re-update the source for the validation to happen
The IsBindingValid dependency property when changed will also update the source to trigger validation
All in all, this works:
/// <summary>
/// Behavior for setting the visual style depending on a validation value
/// </summary>
public class TextBoxValidationBindingBehavior : BehaviorBase<TextBox>
{
#region Internal Validation Class
/// <summary>
/// A validation rule that validates according to a dependency property binding rather than on control content
/// </summary>
private class BindingValidationRule : ValidationRule
{
#region Initialization
/// <summary>
/// Constructor
/// </summary>
/// <param name="that">Behavior holding this class</param>
public BindingValidationRule(TextBoxValidationBindingBehavior that) { this._that = that; }
#endregion
/// <summary>
/// Reference to behavior holding the object
/// </summary>
private readonly TextBoxValidationBindingBehavior _that;
/// <summary>
/// Flag indication that the next validation check is to be disabled / set to true
/// </summary>
public bool DisableValidationOnce = true;
/// <summary>
/// Validates the control
/// </summary>
/// <param name="value">Value to validate (ignored)</param>
/// <param name="cultureInfo">Culture Information</param>
/// <returns>Returns the <see cref="ValidationResult"/> of this validation check</returns>
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (this._that is { } that)
{
ValidationResult validationResult;
if (that.IsBindingValid || this.DisableValidationOnce)
validationResult = new ValidationResult(true, null);
else
validationResult = new ValidationResult(false, that.ErrorText);
// Re-enable validation
this.DisableValidationOnce = false;
// Set Error Tooltip
that.AssociatedObject.ToolTip = validationResult.IsValid ? null : new ToolTip() { Content = validationResult.ErrorContent };
// return result
return validationResult;
}
else throw new Exception($"Internal TextBoxValidationBindingBehavior error.");
}
}
#endregion
#region DepProp: IsBindingValid
public static readonly DependencyProperty IsBindingValidProperty = DependencyProperty.Register("IsBindingValid", typeof(bool), typeof(TextBoxValidationBindingBehavior), new PropertyMetadata(false, IsBindingValidProperty_PropertyChanged));
public bool IsBindingValid
{
get => (bool)this.GetValue(IsBindingValidProperty);
set => this.SetValue(IsBindingValidProperty, value);
}
private static void IsBindingValidProperty_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TextBoxValidationBindingBehavior _this)
{
// Avoid unnecessary notification propagation (the prop probably changed du to us updating the source property)
if (_this._isValidating) return;
// Trigger validation
if (_this.AssociatedObject is { } textBox && textBox.GetBindingExpression(TextBox.TextProperty) is { } bindingExpression)
bindingExpression.UpdateSource();
}
}
#endregion
#region DepProp: ErrorText
public static readonly DependencyProperty ErrorTextProperty = DependencyProperty.Register("ErrorText", typeof(string), typeof(TextBoxValidationBindingBehavior), new PropertyMetadata("Error"));
public string ErrorText
{
get => (string)this.GetValue(ErrorTextProperty);
set => this.SetValue(ErrorTextProperty, value);
}
#endregion
#region Private properties
/// <summary>
/// The custom validation rule to handle bound validation
/// </summary>
private BindingValidationRule _bindingValidationRule { get; set; }
/// <summary>
/// Indicate if validation already happening to avoid verbose notifications in the application
/// </summary>
private bool _isValidating;
#endregion
/// <summary>
/// Setup the behavior
/// </summary>
protected override void OnAttached()
{
base.OnAttached();
// Set handler(s)
this.AssociatedObject.TextChanged += this.AssociatedObject_TextChanged;
// Create custom validation rule
this._bindingValidationRule = new BindingValidationRule(this);
// Add rule
if (this.AssociatedObject is { } textBox && BindingOperations.GetBinding(textBox, TextBox.TextProperty) is { } binding)
{
// We must be able to handle updating the source in order to set value bypassing validation
if (binding.UpdateSourceTrigger == UpdateSourceTrigger.PropertyChanged) throw new Exception("Cannot set UpdateSourceTrigger to PropertyChanged when using TextBoxValidationBindingBehavior");
// Add custom validation rule
binding.ValidationRules.Add(this._bindingValidationRule);
}
}
/// <summary>
/// Set visual state
/// </summary>
private void AssociatedObject_TextChanged(object sender, TextChangedEventArgs e)
{
if (this.AssociatedObject is { } textBox && textBox.GetBindingExpression(TextBox.TextProperty) is { } bindingExpression)
{
this._isValidating = true;
// Remove validation before updating source (or validation will prevent source from updating if it errors)
this._bindingValidationRule.DisableValidationOnce = true;
// Update Source
bindingExpression.UpdateSource();
// Ensure we are not disabled (if UpdateSource did not call Validation)
this._bindingValidationRule.DisableValidationOnce = false;
// Trigger validation
bindingExpression.UpdateSource();
this._isValidating = false;
}
}
/// <summary>
/// Clean-up the behavior
/// </summary>
protected override void OnCleanup()
{
this.AssociatedObject.TextChanged -= this.AssociatedObject_TextChanged;
// Remove rule
if (this.AssociatedObject is { } textBox && BindingOperations.GetBinding(textBox, TextBox.TextProperty) is { } binding)
{
binding.ValidationRules.Remove(this._bindingValidationRule);
}
base.OnCleanup();
}
}
And the XAML code:
<TextBox Grid.Row = "0"
Grid.Column = "1"
Margin = "0, 2, 0, 2"
VerticalAlignment = "Stretch"
VerticalContentAlignment = "Center"
Text = "{Binding NewBookName}">
<b:Interaction.Behaviors>
<behavior:TextBoxValidationBindingBehavior IsBindingValid = "{Binding IsValidName}"
ErrorText = "Invalid name or a book with the same name already exists."/>
</b:Interaction.Behaviors>
</TextBox>
However, there are a few things I really don't like about this way of proceeding:
This procedure is very verbose in that it triggers potentially a lot of notification bindings every time it updates the source just to validate the content
It may not be thread-safe?!
While it is theoretically possible to use it with other validation rules, it would probably need a lot of code to get it to work
I find this quite hacky...
I hope this can help others or if one has a better idea: YOU ARE WELCOME!!! 😊
Very high up the visual tree in WPF's Expander control is a border element (see screenshot). By default this has a CornerRadius of 3. Is it possible to modify this value?
I'll leave marking as answer for now but I managed to implement the solution as follows:
Using stylesnooper I obtained the style / control template used for the 'standard' Expander control.
Then after discovering it didn't quite behave as expected, figured out that the line <ToggleButton IsChecked="False" ... is wrong and should actually be <ToggleButton IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"...
Everything then worked as expected.
I made a behavior which modifies the first found border in the ControlTemplate. You can easily extend the behavior with new properties where u want to modify
/// <summary>
/// modifies the first found <see cref="Border"/> in the <see cref="ControlTemplate"/> of the attached <see cref="Control"/>
/// </summary>
public class ModifyBorderBehavior : Behavior<Control>
{
// ##############################################################################################################################
// Properties
// ##############################################################################################################################
#region Properties
/// <summary>
/// The new corner radius
/// </summary>
public CornerRadius CornerRadius
{
get => (CornerRadius)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
/// <summary>
/// The <see cref="CornerRadius"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(ModifyBorderBehavior));
#endregion
// ##############################################################################################################################
// Constructor
// ##############################################################################################################################
#region Constructor
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += _OnLoaded;
}
private void _OnLoaded(object sender, RoutedEventArgs e)
{
//var children = VisualTree.GetVisualChildCollection<Border>(sender);
if (sender is Control control)
{
Border border = VisualTree.GetVisualChild<Border>(control);
if(ReadLocalValue(CornerRadiusProperty) != DependencyProperty.UnsetValue)
border.CornerRadius = CornerRadius;
}
}
#endregion
}
<Expander>
<i:Interaction.Behaviors>
<zls:ModifyBorderBehavior CornerRadius="0"/>
</i:Interaction.Behaviors>
</Expander>
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.
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");
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.