How to track number of spam clicks on a button in WPF - wpf

I have a button that skips a video on by x seconds. if a user spam clicks that button my video updates over and over again which is an expensive operation. What is the best way to stop a user spamming the button? I am using a routed ui command and want to add up the seconds and do 1 operation. Is a delay timer best practice here? delay the operation for 10ms and reset the delay on every click? or is there something built into wpf that can help?
UPDATE:
I would like to track the number of clicks a user is making during the spam click of the button

I really hope the async way works, while we were trying that out i created a solution, feel free to tell me all i did wrong and any bad practices.
I decided to use dispatcher timer for this even though i didn't really want to. couldn't find any better practices online.
private TimeSpan overallSkipSpeed = TimeSpan.Zero;
private readonly TimeSpan Interval = TimeSpan.FromMilliseconds(400);
private DispatcherTimer _dispatcherTimer;
private TimeSpan _time;
// Execute command function
public void ExecuteClickCommand()
{
// If the timer isn't going create and start it
if (_dispatcherTimer == null)
{
overallSkipSpeed = TimeSpanToModifyBy(skipSpeed, skipForward);
_time = Interval;
_dispatcherTimer = new DispatcherTimer(Interval, DispatcherPriority.Normal, Tick, Application.Current.Dispatcher);
_dispatcherTimer.Start();
}
else // If the timer is going reset to interval
{
// THIS IS WHERE I ADDED MY SKIP VALUES TOGETHER
// So value from last click + value from this click
_dispatcherTimer.Stop();
_time = Interval;
_dispatcherTimer.Start();
}
}
// Method to run when timer ticks over
private void Tick(object sender, EventArgs eventArgs)
{
// if the timer has reached below zero
if (_time <= TimeSpan.Zero)
{
_dispatcherTimer.Stop();
_dispatcherTimer = null;
_time = TimeSpan.FromSeconds(0);
// HERE IS WHERE WE CAN NOW SKIP VIDEO BY
// THE SKIP SPEED WE HAVE ACCUMULATED
}
else
{
_time = _time.Add(-Interval);
}
}

I have gone one step further with this and created my own command.
This command works like a relay command but will delay if you set a delay time span in initialisation. you can retrieve the number of times it was clicked in your execute method.
Initialise the command:
ICommand DelayedClickCommand = new DelayedCommand(ExecuteDelayedClickCommand, TimeSpan.FromMilliseconds(200));
Create an execute method and retrive the amount of times clicked:
private void ExecuteClickCommand()
{
TimesClicked = ((DelayedCommand)ClickCommand).TimesClicked;
}
and here is the command class:
public class DelayedCommand : ICommand
{
private readonly Action _methodToExecute;
private readonly Func<bool> _canExecuteEvaluator;
private readonly DispatcherTimer _dispatcherTimer;
public int TimesClicked;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
/// <summary>
/// A command to stop the spamming of the <see cref="Execute"/> method
/// </summary>
/// <param name="methodToExecute">Method to run when command executes</param>
/// <param name="canExecuteEvaluator">Method used to determine if the command can execute</param>
/// <param name="delayTime">The cool down period required between click execution</param>
public DelayedCommand(Action methodToExecute, Func<bool> canExecuteEvaluator, TimeSpan delayTime)
{
_methodToExecute = methodToExecute;
_canExecuteEvaluator = canExecuteEvaluator;
_dispatcherTimer = new DispatcherTimer(delayTime, DispatcherPriority.Normal, Callback, Application.Current.Dispatcher);
}
/// <summary>
/// A command to stop the spamming of the <see cref="Execute"/> method
/// when no <see cref="CanExecute"/> method is required
/// </summary>
/// <param name="methodToExecute">Method to run when command executes</param>
/// <param name="delayTime">The cool down period required between click execution</param>
public DelayedCommand(Action methodToExecute, TimeSpan delayTime)
: this(methodToExecute, null, delayTime)
{
}
/// <summary>
/// A command when only a <see cref="Execute"/> method is needed
/// </summary>
/// <param name="methodToExecute">Method to run when command executes</param>
public DelayedCommand(Action methodToExecute)
: this(methodToExecute, null, TimeSpan.Zero)
{
}
/// <summary>
/// A command taking a <see cref="Execute"/> Method and a <see cref="CanExecute"/> method
/// </summary>
/// <param name="methodToExecute">Method to run when command executes</param>
/// <param name="canExecuteEvaluator">Method used to determine if the command can execute</param>
public DelayedCommand(Action methodToExecute, Func<bool> canExecuteEvaluator)
: this(methodToExecute, canExecuteEvaluator, TimeSpan.Zero)
{
}
public bool CanExecute(object parameter)
{
if (_canExecuteEvaluator == null)
{
return true;
}
return _canExecuteEvaluator.Invoke();
}
public void Execute(object parameter)
{
if (!_dispatcherTimer.IsEnabled)
TimesClicked = 0;
TimesClicked++;
_dispatcherTimer?.Stop();
_dispatcherTimer?.Start();
}
private void Callback(object sender, EventArgs eventArgs)
{
_dispatcherTimer.Stop();
_methodToExecute.Invoke();
}
}
Note: that when you spam click this command execute will not run untill 200ms after the last click was performed, giving a lagging effect. I have added a sample project to git hub and will add better commands for this question on there
https://github.com/sgreaves1/DelayedCommands

Guess i'm the laziest person here...
public class PostponeCommand : ICommand
{
private TimeSpan _delay;
private Action<object> _command;
private CancellationTokenSource _cancellation;
public PostponeCommand(Action<object> command, int delayMs)
{
this._command = command;
this._delay = TimeSpan.FromMilliseconds(delayMs);
}
public bool CanExecute(object parameter)
{
return true;
}
public async void Execute(object parameter)
{
_cancellation?.Cancel();
_cancellation = new CancellationTokenSource();
try
{
await Task.Delay(_delay, _cancellation.Token);
_command?.Invoke(parameter);
}
catch (TaskCanceledException ex)
{
// canceled
}
}
public event EventHandler CanExecuteChanged;
}

Not sure about build-in Command ability to do this, but you can do it with delay (updated based on comments):
private int spamCount = 0;
private int delayValue = 0;
private object isHoldedLock = new object();
private bool isHolded = false;
public bool CanProceed(int delay, Action updateVideo)
{
lock (this.isHoldedLock)
{
if (this.isHolded)
{
this.spamCount++;
this.delayValue = delay;
return false;
}
this.isHolded = true;
this.delayValue = delay;
Task.Run(async () =>
{
while (this.delayValue > 0)
{
await Task.Delay(100);
this.delayValue -= 100;
}
updateVideo();
lock (this.isHoldedLock)
{
this.isHolded = false;
}
});
return true;
}
}
Process/reset spamCount value inside SkipVideo any way you need.
And using in your command handler:
private void InvokedFromCommand()
{
if (CanProceed(1000, SkipVideo()))
{
// SkipVideo();
}
}

Related

WPF ListView with large collection hangs GUI

++++++ Link to example project ++++++
I have a file that can contain thousands of lines of logged messages. I am parsing this file and adding each line (as a log event) to a collection. This collection should then be shown in a ListView.
As below:
<ListView
Grid.Row="0"
Margin="5"
ItemsSource="{Binding SelectedSerilogFileLog.LogEvents}"
ScrollViewer.CanContentScroll="False"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectedItem="{Binding SelectedLogEvent}"
SelectionMode="Single">
The parsing of the file (this one contains 2500+ log events) and adding to the collection takes around 100ms. Then when the bound collection is updated with the ReplaceContent method (this suppresses the collectionchanged event firing on every item added) the GUI hangs, but I cannot see why or what can be causing this.
MainWindow.cs
...
/// <summary>
///
/// </summary>
public SerilogFileLog SelectedSerilogFileLog
{
get => selectedSerilogFileLog; set
{
if (selectedSerilogFileLog != null)
{
SelectedSerilogFileLog.OnSerilogParserFinished -= OnSerilogParserFinished;
SelectedSerilogFileLog.OnSerilogParserProgressChanged -= OnSerilogParserProgressChanged;
}
selectedSerilogFileLog = value;
if (selectedSerilogFileLog != null)
{
ParserProgress = 0;
SelectedSerilogFileLog.OnSerilogParserFinished += OnSerilogParserFinished;
SelectedSerilogFileLog.OnSerilogParserProgressChanged += OnSerilogParserProgressChanged;
sw.Start();
SelectedSerilogFileLog.Parse();
}
NotifyPropertyChanged(nameof(SelectedSerilogFileLog));
}
}
...
private void Button_Click(object sender, RoutedEventArgs e)
{
SelectedSerilogFileLog = null;
SelectedSerilogFileLog = new SerilogFileLog() { FilePath = "Application20210216.log" };
}
The parsing and loading of the items occurs in a separate Task.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace LargeListViewTest.Classes
{
public class SerilogFileLog : INotifyPropertyChanged
{
private LogEvent lastLogEvent;
private ObservableCollectionEx<LogEvent> logEvents;
private string name;
private string description;
private string filePath;
private Regex patternMatching;
private string matchExpression = #"^(?<DateTime>[^|]+)\| (?<Level>[^|]+) \| (?<MachineName>[^|]+) \| (?<Source>[^|]+) \| (?<Message>[^$]*)$";
public delegate void SerilogParserProgressHandler(int Percentage);
public delegate void SerilogParserFinishedHandler();
/// <summary>
///
/// </summary>
public event SerilogParserProgressHandler OnSerilogParserProgressChanged;
/// <summary>
///
/// </summary>
public event SerilogParserFinishedHandler OnSerilogParserFinished;
/// <summary>
/// Gets or sets the LogEvents.
/// </summary>
public ObservableCollectionEx<LogEvent> LogEvents { get => logEvents; private set { logEvents = value; NotifyPropertyChanged(nameof(LogEvents)); } }
/// <summary>
/// Gets or sets the Name.
/// </summary>
public string Name { get => name; private set { name = value; NotifyPropertyChanged(nameof(Name)); } }
/// <summary>
/// Gets or sets the Description.
/// </summary>
public string Description { get => description; private set { description = value; NotifyPropertyChanged(nameof(Description)); } }
/// <summary>
/// Gets or sets the FilePath.
/// </summary>
public string FilePath
{
get => filePath;
set
{
filePath = value;
Name = Path.GetFileNameWithoutExtension(value);
Description = FilePath;
}
}
/// <summary>
///
/// </summary>
public SerilogFileLog()
{
LogEvents = new ObservableCollectionEx<LogEvent>();
patternMatching = new Regex(matchExpression, RegexOptions.Singleline | RegexOptions.Compiled);
}
/// <summary>
///
/// </summary>
public void Parse()
{
Task task = Task.Factory.StartNew(() => { InternalParse(); });
}
/// <summary>
///
/// </summary>
private void InternalParse()
{
OnSerilogParserProgressChanged?.Invoke(0);
try
{
if (!string.IsNullOrWhiteSpace(FilePath))
{
Console.WriteLine("Starting parse for {0}", FilePath);
long currentLength = 0;
FileInfo fi = new FileInfo(FilePath);
if (fi.Exists)
{
Console.WriteLine("Parsing Serilog file: {0}.", FilePath);
fi.Refresh();
List<LogEvent> parsedLogEvents = new List<LogEvent>();
StringBuilder sb = new StringBuilder();
using (FileStream fileStream = fi.Open(FileMode.Open, FileAccess.Read, FileShare.Write))
using (var streamReader = new StreamReader(fileStream))
{
while (streamReader.Peek() != -1)
{
sb.Append(streamReader.ReadLine());
LogEvent newLogEvent = ParseLogEvent(sb.ToString());
if (newLogEvent != null)
{
parsedLogEvents.Add(newLogEvent);
lastLogEvent = newLogEvent;
}
OnSerilogParserProgressChanged?.Invoke((int)(currentLength * 100 / fi.Length));
currentLength = currentLength + sb.ToString().Length;
sb.Clear();
}
}
LogEvents.ReplaceContent(parsedLogEvents);
}
Console.WriteLine("Finished parsing Serilog {0}.", FilePath);
}
}
catch (Exception ex)
{
Console.WriteLine("Error parsing Serilog." + ex.Message);
}
OnSerilogParserProgressChanged?.Invoke(100);
SerilogParserFinishedHandler onSerilogParserFinished = OnSerilogParserFinished;
if (onSerilogParserFinished == null)
return;
OnSerilogParserFinished();
}
/// <summary>
///
/// </summary>
/// <param name="mes"></param>
/// <returns></returns>
private LogEvent ParseLogEvent(string mes)
{
LogEvent logEvent = new LogEvent();
Match matcher = patternMatching.Match(mes);
try
{
if (matcher.Success)
{
logEvent.Message = matcher.Groups["Message"].Value;
DateTime dt;
if (!DateTime.TryParse(matcher.Groups["DateTime"].Value, out dt))
{
Console.WriteLine("Failed to parse date {Value}", matcher.Groups["DateTime"].Value);
}
logEvent.DateTime = dt;
logEvent.Level = matcher.Groups["Level"].Value;
logEvent.MachineName = matcher.Groups["MachineName"].Value;
logEvent.Source = matcher.Groups["Source"].Value;
}
else
{
if ((string.IsNullOrEmpty(mes) || (!Char.IsDigit(mes[0])) || !Char.IsDigit(mes[1])) && lastLogEvent != null)
{
// seems to be a continuation of the previous line, add it to the last event.
lastLogEvent.Message += Environment.NewLine;
lastLogEvent.Message += mes;
logEvent = null;
}
else
{
Console.WriteLine("Message parsing failed.");
}
if (logEvent != null)
logEvent.Message = mes;
}
}
catch (Exception ex)
{
Console.WriteLine("ParseLogEvent exception." + ex.Message);
}
return logEvent;
}
#region INotify
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string p) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(p));
#endregion
}
}
I have an ObservableCollectionEx class that extends the default ObservableCollection, this class suppresses the collection changed event until all the items have been added/replaced.
/// <summary>
/// Adds the supplied items to the collection and raises a single <see cref="CollectionChanged"/> event
/// when the operation is complete.
/// </summary>
/// <param name="items">The items to add.</param>
public void AddRange(IEnumerable<T> items, bool notifyAfter = true)
{
if (null == items)
{
throw new ArgumentNullException("items");
}
if (items.Any())
{
try
{
SuppressChangeNotification();
CheckReentrancy();
foreach (var item in items)
{
Add(item);
}
}
finally
{
if (notifyAfter)
FireChangeNotification();
suppressOnCollectionChanged = false;
}
}
}
/// <summary>
/// Replaces the content of the collection with the supplied items and raises a single <see cref="CollectionChanged"/> event
/// when the operation is complete.
/// </summary>
/// <param name="items">The items to replace the current content.</param>
public void ReplaceContent(IEnumerable<T> items)
{
SuppressChangeNotification();
ClearItems();
AddRange(items);
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!suppressOnCollectionChanged)
{
#if NoCrossThreadSupport
base.OnCollectionChanged(e);
#else
using (BlockReentrancy())
{
NotifyCollectionChangedEventHandler eventHandler = CollectionChanged;
if (eventHandler == null)
return;
Delegate[] delegates = eventHandler.GetInvocationList();
// Walk the invocation list
foreach (NotifyCollectionChangedEventHandler handler in delegates)
{
DispatcherObject dispatcherObject = handler.Target as DispatcherObject;
// If the subscriber is a DispatcherObject and different thread
if (dispatcherObject != null && !dispatcherObject.CheckAccess())
{
// Invoke handler in the target dispatcher's thread
dispatcherObject.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, this, e);
}
else // Execute handler as is
handler(this, e);
}
}
#endif
}
}
I have tried using a List but I got the same behaviour.
Any ideas?

How determine fault StoryBoard

The initial problem is enough known - "Cannot animate '(0).(1)' on an immutable object instance".
There are many questions here in SO about it but all the solutions are more fixes or crutches. And most of questions are linked to concrete part of code.
Also there are few topics about this problem with possible causes:
https://wpftutorial.net/DebuggingAnimations.html
https://blogs.msdn.microsoft.com/mikehillberg/2006/09/25/tip-cannot-animate-on-an-immutable-object-instance/
I have huge corporate app where a have hundreds styles and storyboards. I can't disable them step by step and it's painstaking work to looking for problem part of code.
I look at these bug not from side of looking for in many xamls but from side of loging. I tried to research info in InvalidOperationException that is raised but there is no useful info like control place in xaml or smth else.
Also one idea is to create class inherited from Storyboard and to override methods.
But there is no methods to override.
Can someone propose how to log the internality of storyboard or other class that is responsible of animation?
At last I found sulution.
You should add classes: listener to animation, AttachedProperty and custom StoryBoard.
public static class TriggerTracing
{
static TriggerTracing()
{
// Initialise WPF Animation tracing and add a TriggerTraceListener
PresentationTraceSources.Refresh();
PresentationTraceSources.AnimationSource.Listeners.Clear();
PresentationTraceSources.AnimationSource.Listeners.Add(new TriggerTraceListener());
PresentationTraceSources.AnimationSource.Switch.Level = SourceLevels.All;
}
#region TriggerName attached property
/// <summary>
/// Gets the trigger name for the specified trigger. This will be used
/// to identify the trigger in the debug output.
/// </summary>
/// <param name="trigger">The trigger.</param>
/// <returns></returns>
public static string GetTriggerName(TriggerBase trigger)
{
return (string)trigger.GetValue(TriggerNameProperty);
}
/// <summary>
/// Sets the trigger name for the specified trigger. This will be used
/// to identify the trigger in the debug output.
/// </summary>
/// <param name="trigger">The trigger.</param>
/// <returns></returns>
public static void SetTriggerName(TriggerBase trigger, string value)
{
trigger.SetValue(TriggerNameProperty, value);
}
public static readonly DependencyProperty TriggerNameProperty =
DependencyProperty.RegisterAttached(
"TriggerName",
typeof(string),
typeof(TriggerTracing),
new UIPropertyMetadata(string.Empty));
#endregion
#region TraceEnabled attached property
/// <summary>
/// Gets a value indication whether trace is enabled for the specified trigger.
/// </summary>
/// <param name="trigger">The trigger.</param>
/// <returns></returns>
public static bool GetTraceEnabled(TriggerBase trigger)
{
return (bool)trigger.GetValue(TraceEnabledProperty);
}
/// <summary>
/// Sets a value specifying whether trace is enabled for the specified trigger
/// </summary>
/// <param name="trigger"></param>
/// <param name="value"></param>
public static void SetTraceEnabled(TriggerBase trigger, bool value)
{
trigger.SetValue(TraceEnabledProperty, value);
}
public static readonly DependencyProperty TraceEnabledProperty =
DependencyProperty.RegisterAttached(
"TraceEnabled",
typeof(bool),
typeof(TriggerTracing),
new UIPropertyMetadata(false, OnTraceEnabledChanged));
private static void OnTraceEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var triggerBase = d as EventTrigger;
if (triggerBase == null)
return;
if (!(e.NewValue is bool))
return;
if ((bool)e.NewValue)
{
// insert dummy story-boards which can later be traced using WPF animation tracing
var storyboard = new TriggerTraceStoryboard(triggerBase, TriggerTraceStoryboardType.Enter);
triggerBase.Actions.Insert(0, new BeginStoryboard() { Storyboard = storyboard });
//storyboard = new TriggerTraceStoryboard(triggerBase, TriggerTraceStoryboardType.Exit);
//triggerBase.ExitActions.Insert(0, new BeginStoryboard() { Storyboard = storyboard });
}
else
{
// remove the dummy storyboards
//foreach (TriggerActionCollection actionCollection in new[] { triggerBase.EnterActions, triggerBase.ExitActions })
foreach (TriggerActionCollection actionCollection in new[] { triggerBase.Actions })
{
foreach (TriggerAction triggerAction in actionCollection)
{
BeginStoryboard bsb = triggerAction as BeginStoryboard;
if (bsb != null && bsb.Storyboard != null && bsb.Storyboard is TriggerTraceStoryboard)
{
actionCollection.Remove(bsb);
break;
}
}
}
}
}
#endregion
private enum TriggerTraceStoryboardType
{
Enter, Exit
}
/// <summary>
/// A dummy storyboard for tracing purposes
/// </summary>
private class TriggerTraceStoryboard : Storyboard
{
public TriggerTraceStoryboardType StoryboardType { get; private set; }
public TriggerBase TriggerBase { get; private set; }
public TriggerTraceStoryboard(TriggerBase triggerBase, TriggerTraceStoryboardType storyboardType)
{
TriggerBase = triggerBase;
StoryboardType = storyboardType;
}
}
/// <summary>
/// A custom tracelistener.
/// </summary>
private class TriggerTraceListener : TraceListener
{
public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args)
{
base.TraceEvent(eventCache, source, eventType, id, format, args);
if (format.StartsWith("Storyboard has begun;"))
{
TriggerTraceStoryboard storyboard = args[1] as TriggerTraceStoryboard;
if (storyboard != null)
{
// add a breakpoint here to see when your trigger has been
// entered or exited
// the element being acted upon
object targetElement = args[5];
// the namescope of the element being acted upon
INameScope namescope = (INameScope)args[7];
TriggerBase triggerBase = storyboard.TriggerBase;
string triggerName = GetTriggerName(storyboard.TriggerBase);
var str = "";
var element = targetElement as DependencyObject;
while (element != null)
{
str += element.ToString() + Environment.NewLine;
element = VisualTreeHelper.GetParent(element);
}
LoggingInfrastructure.DefaultLogger.Log(...);
}
}
}
public override void Write(string message)
{
}
public override void WriteLine(string message)
{
}
}
Then you could add property to xaml where you need:
<EventTrigger Ui:TriggerTracing.TriggerName="CopyTextBlockStyle PreviewMouseLeftButtonDown"
Ui:TriggerTracing.TraceEnabled="True" RoutedEvent="PreviewMouseLeftButtonDown">

Fast performing and thread safe observable collection

ObservableCollections raise notifications for each action performed on them. Firstly they dont have bulk add or remove calls, secondly they are not thread safe.
Doesn't this make them slower? Cant we have a faster alternative? Some say ICollectionView wrapped around an ObservableCollection is fast? How true is this claim.
ObservableCollection can be fast, if it wants to. :-)
The code below is a very good example of a thread safe, faster observable collection and you can extend it further to your wish.
using System.Collections.Specialized;
public class FastObservableCollection<T> : ObservableCollection<T>
{
private readonly object locker = new object();
/// <summary>
/// This private variable holds the flag to
/// turn on and off the collection changed notification.
/// </summary>
private bool suspendCollectionChangeNotification;
/// <summary>
/// Initializes a new instance of the FastObservableCollection class.
/// </summary>
public FastObservableCollection()
: base()
{
this.suspendCollectionChangeNotification = false;
}
/// <summary>
/// This event is overriden CollectionChanged event of the observable collection.
/// </summary>
public override event NotifyCollectionChangedEventHandler CollectionChanged;
/// <summary>
/// This method adds the given generic list of items
/// as a range into current collection by casting them as type T.
/// It then notifies once after all items are added.
/// </summary>
/// <param name="items">The source collection.</param>
public void AddItems(IList<T> items)
{
lock(locker)
{
this.SuspendCollectionChangeNotification();
foreach (var i in items)
{
InsertItem(Count, i);
}
this.NotifyChanges();
}
}
/// <summary>
/// Raises collection change event.
/// </summary>
public void NotifyChanges()
{
this.ResumeCollectionChangeNotification();
var arg
= new NotifyCollectionChangedEventArgs
(NotifyCollectionChangedAction.Reset);
this.OnCollectionChanged(arg);
}
/// <summary>
/// This method removes the given generic list of items as a range
/// into current collection by casting them as type T.
/// It then notifies once after all items are removed.
/// </summary>
/// <param name="items">The source collection.</param>
public void RemoveItems(IList<T> items)
{
lock(locker)
{
this.SuspendCollectionChangeNotification();
foreach (var i in items)
{
Remove(i);
}
this.NotifyChanges();
}
}
/// <summary>
/// Resumes collection changed notification.
/// </summary>
public void ResumeCollectionChangeNotification()
{
this.suspendCollectionChangeNotification = false;
}
/// <summary>
/// Suspends collection changed notification.
/// </summary>
public void SuspendCollectionChangeNotification()
{
this.suspendCollectionChangeNotification = true;
}
/// <summary>
/// This collection changed event performs thread safe event raising.
/// </summary>
/// <param name="e">The event argument.</param>
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
// Recommended is to avoid reentry
// in collection changed event while collection
// is getting changed on other thread.
using (BlockReentrancy())
{
if (!this.suspendCollectionChangeNotification)
{
NotifyCollectionChangedEventHandler eventHandler =
this.CollectionChanged;
if (eventHandler == null)
{
return;
}
// Walk thru invocation list.
Delegate[] delegates = eventHandler.GetInvocationList();
foreach
(NotifyCollectionChangedEventHandler handler in delegates)
{
// If the subscriber is a DispatcherObject and different thread.
DispatcherObject dispatcherObject
= handler.Target as DispatcherObject;
if (dispatcherObject != null
&& !dispatcherObject.CheckAccess())
{
// Invoke handler in the target dispatcher's thread...
// asynchronously for better responsiveness.
dispatcherObject.Dispatcher.BeginInvoke
(DispatcherPriority.DataBind, handler, this, e);
}
else
{
// Execute handler as is.
handler(this, e);
}
}
}
}
}
}
Also ICollectionView that sits above the ObservableCollection is actively aware of the changes and performs filtering, grouping, sorting relatively fast as compared to any other source list.
Again observable collections may not be a perfect answer for faster data updates but they do their job pretty well.
Here is a compilation of some solutions which I made. The idea of collection changed invokation taken from first answer.
Also seems that "Reset" operation should be synchronous with main thread otherwise strange things happen to CollectionView and CollectionViewSource.
I think that's because on "Reset" handler tries to read the collection contents immediately and they should be already in place.
If you do "Reset" async and than immediately add some items also async than newly added items might be added twice.
public interface IObservableList<T> : IList<T>, INotifyCollectionChanged
{
}
public class ObservableList<T> : IObservableList<T>
{
private IList<T> collection = new List<T>();
public event NotifyCollectionChangedEventHandler CollectionChanged;
private ReaderWriterLock sync = new ReaderWriterLock();
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
if (CollectionChanged == null)
return;
foreach (NotifyCollectionChangedEventHandler handler in CollectionChanged.GetInvocationList())
{
// If the subscriber is a DispatcherObject and different thread.
var dispatcherObject = handler.Target as DispatcherObject;
if (dispatcherObject != null && !dispatcherObject.CheckAccess())
{
if ( args.Action == NotifyCollectionChangedAction.Reset )
dispatcherObject.Dispatcher.Invoke
(DispatcherPriority.DataBind, handler, this, args);
else
// Invoke handler in the target dispatcher's thread...
// asynchronously for better responsiveness.
dispatcherObject.Dispatcher.BeginInvoke
(DispatcherPriority.DataBind, handler, this, args);
}
else
{
// Execute handler as is.
handler(this, args);
}
}
}
public ObservableList()
{
}
public void Add(T item)
{
sync.AcquireWriterLock(Timeout.Infinite);
try
{
collection.Add(item);
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, item));
}
finally
{
sync.ReleaseWriterLock();
}
}
public void Clear()
{
sync.AcquireWriterLock(Timeout.Infinite);
try
{
collection.Clear();
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset));
}
finally
{
sync.ReleaseWriterLock();
}
}
public bool Contains(T item)
{
sync.AcquireReaderLock(Timeout.Infinite);
try
{
var result = collection.Contains(item);
return result;
}
finally
{
sync.ReleaseReaderLock();
}
}
public void CopyTo(T[] array, int arrayIndex)
{
sync.AcquireWriterLock(Timeout.Infinite);
try
{
collection.CopyTo(array, arrayIndex);
}
finally
{
sync.ReleaseWriterLock();
}
}
public int Count
{
get
{
sync.AcquireReaderLock(Timeout.Infinite);
try
{
return collection.Count;
}
finally
{
sync.ReleaseReaderLock();
}
}
}
public bool IsReadOnly
{
get { return collection.IsReadOnly; }
}
public bool Remove(T item)
{
sync.AcquireWriterLock(Timeout.Infinite);
try
{
var index = collection.IndexOf(item);
if (index == -1)
return false;
var result = collection.Remove(item);
if (result)
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
return result;
}
finally
{
sync.ReleaseWriterLock();
}
}
public IEnumerator<T> GetEnumerator()
{
return collection.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return collection.GetEnumerator();
}
public int IndexOf(T item)
{
sync.AcquireReaderLock(Timeout.Infinite);
try
{
var result = collection.IndexOf(item);
return result;
}
finally
{
sync.ReleaseReaderLock();
}
}
public void Insert(int index, T item)
{
sync.AcquireWriterLock(Timeout.Infinite);
try
{
collection.Insert(index, item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
}
finally
{
sync.ReleaseWriterLock();
}
}
public void RemoveAt(int index)
{
sync.AcquireWriterLock(Timeout.Infinite);
try
{
if (collection.Count == 0 || collection.Count <= index)
return;
var item = collection[index];
collection.RemoveAt(index);
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove, item, index));
}
finally
{
sync.ReleaseWriterLock();
}
}
public T this[int index]
{
get
{
sync.AcquireReaderLock(Timeout.Infinite);
try
{
var result = collection[index];
return result;
}
finally
{
sync.ReleaseReaderLock();
}
}
set
{
sync.AcquireWriterLock(Timeout.Infinite);
try
{
if (collection.Count == 0 || collection.Count <= index)
return;
var item = collection[index];
collection[index] = value;
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace, value, item, index));
}
finally
{
sync.ReleaseWriterLock();
}
}
}
}
I can't add comments because I'm not cool enough yet, but sharing this issue I ran into is probably worth posting even though it's not really an answer. I kept getting an "Index was out of range" exception using this FastObservableCollection, because of the BeginInvoke. Apparently changes being notified can be undone before the handler is called, so to fix this I passed the following as the fourth parameter for the BeginInvoke called from the OnCollectionChanged method (as opposed to using the event args one):
dispatcherObject.Dispatcher.BeginInvoke
(DispatcherPriority.DataBind, handler, this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
Instead of this:
dispatcherObject.Dispatcher.BeginInvoke
(DispatcherPriority.DataBind, handler, this, e);
This fixed the "Index was out of range" issue I was running into. Here's a more detailed explaination / code snpipet: Where do I get a thread-safe CollectionView?
An example where is created a synchronized Observable list:
newSeries = new XYChart.Series<>();
ObservableList<XYChart.Data<Number, Number>> listaSerie;
listaSerie = FXCollections.synchronizedObservableList(FXCollections.observableList(new ArrayList<XYChart.Data<Number, Number>>()));
newSeries.setData(listaSerie);

unit test an attached behavior wpf

I am still grokking attached behaviors in general, and am at a loss to see how to write a unit test for one.
I pasted some code below from Sacha Barber's Cinch framework that allows a window to be closed via attached behavior. Can somewone show me an example unit test for it?
Thanks!
Berryl
#region Close
/// <summary>Dependency property which holds the ICommand for the Close event</summary>
public static readonly DependencyProperty CloseProperty =
DependencyProperty.RegisterAttached("Close",
typeof(ICommand), typeof(Lifetime),
new UIPropertyMetadata(null, OnCloseEventInfoChanged));
/// <summary>Attached Property getter to retrieve the CloseProperty ICommand</summary>
public static ICommand GetClose(DependencyObject source)
{
return (ICommand)source.GetValue(CloseProperty);
}
/// <summary>Attached Property setter to change the CloseProperty ICommand</summary>
public static void SetClose(DependencyObject source, ICommand command)
{
source.SetValue(CloseProperty, command);
}
/// <summary>This is the property changed handler for the Close property.</summary>
private static void OnCloseEventInfoChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var win = sender as Window;
if (win == null) return;
win.Closing -= OnWindowClosing;
win.Closed -= OnWindowClosed;
if (e.NewValue == null) return;
win.Closing += OnWindowClosing;
win.Closed += OnWindowClosed;
}
/// <summary>
/// This method is invoked when the Window.Closing event is raised.
/// It checks with the ICommand.CanExecute handler
/// and cancels the event if the handler returns false.
/// </summary>
private static void OnWindowClosing(object sender, CancelEventArgs e)
{
var dpo = (DependencyObject)sender;
var ic = GetClose(dpo);
if (ic == null) return;
e.Cancel = !ic.CanExecute(GetCommandParameter(dpo));
}
/// <summary>
/// This method is invoked when the Window.Closed event is raised.
/// It executes the ICommand.Execute handler.
/// </summary>
static void OnWindowClosed(object sender, EventArgs e)
{
var dpo = (DependencyObject)sender;
var ic = GetClose(dpo);
if (ic == null) return;
ic.Execute(GetCommandParameter(dpo));
}
#endregion
You would likely use a lambda in your ICommand using a DelegateCommand or a RelayCommand. Multiple implementations of these exists all over the place and Cinch may have something similar. Really simple version (as an example, not meant for production use):
public class DelegateCommand : ICommand {
private Action _execute = null;
public void Execute( object parameter ) {
_execute();
}
public DelegateCommand( Action execute ) {
_execute = execute;
}
#region stuff that doesn't affect functionality
public bool CanExecute( object parameter ) {
return true;
}
public event EventHandler CanExecuteChanged {
add { }
remove { }
}
#endregion
}
Then your test body might look something like this:
bool wascalled = false;
var execute = new DelegateCommand(
() => {
wascalled = true;
} );
var window = new Window();
SomeClass.SetClose( window, execute );
// does the window need to be shown for Close() to work? Nope.
window.Close();
AssertIsTrue( wascalled );
This is an over-simplified example. There are of course other tests you'll want to perform, in which case you should create or find a fuller implementation of DelegateCommand that also properly implements CanExecute, among other things.
DependencyProperty changing and value coercion on their own looks like 'Impossible Dependencies' for me. Having reference to Window there makes things even trickier. I think I'd go with Humble Object pattern here...

CommandManager.InvalidateRequerySuggested() isn't fast enough. What can I do?

Short Version
Calls to CommandManager.InvalidateRequerySuggested() take far longer to take effect than I would like (1-2 second delay before UI controls become disabled).
Long Version
I have a system where I submit tasks to a background-thread based task processor. This submit happens on the WPF UI thread.
When this submit happens, the object that manages my background thread does two things:
It raises a "busy" event (still on the UI thread) that several view models respond to; when they receive this event, they set an IsEnabled flag on themselves to false. Controls in my views, which are databound to this property, are immediately grayed out, which is what I would expect.
It informs my WPF ICommand objects that they should not be allowed to execute (again, still on the UI thread). Because there is nothing like INotifyPropertyChanged for ICommand objects, I am forced to call CommandManager.InvalidateRequerySuggested() to force WPF to reconsider all of my command objects' CanExecute states (yes, I actually do need to do this: otherwise, none of these controls become disabled). Unlike item 1, though, it takes a significantly longer time for my buttons/menu items/etc that are using ICommand objects to visually change to a disabled state than it does for the UI controls that have their IsEnabled property manually set.
The problem is, from a UX point of view, this looks awful; half of my controls are immediately grayed out (because their IsEnabled property is set to false), and then a full 1-2 seconds later, the other half of my controls follow suit (because their CanExecute methods are finally re-evaluated).
So, part 1 of my question:
As silly as it sounds to ask, is there a way I can make CommandManager.InvalidateRequerySuggested() do it's job faster? I suspect that there isn't.
Fair enough, part 2 of my question:
How can I work around this? I'd prefer all of my controls be disabled at the same time. It just looks unprofessional and awkward otherwise. Any ideas? :-)
CommandManager.InvalidateRequerySuggested() tries to validate all commands, which is totally ineffective (and in your case slow) - on every change, you are asking every command to recheck its CanExecute()!
You'd need the command to know on which objects and properties is its CanExecute dependent, and suggest requery only when they change. That way, if you change a property of an object, only commands that depend on it will change their state.
This is how I solved the problem, but at first, a teaser:
// in ViewModel's constructor - add a code to public ICommand:
this.DoStuffWithParameterCommand = new DelegateCommand<object>(
parameter =>
{
//do work with parameter (remember to check against null)
},
parameter =>
{
//can this command execute? return true or false
}
)
.ListenOn(whichObject, n => n.ObjectProperty /*type safe!*/, this.Dispatcher /*we need to pass UI dispatcher here*/)
.ListenOn(anotherObject, n => n.AnotherObjectProperty, this.Dispatcher); // chain calling!
The command is listening on NotifyPropertyChanged events from object that affect whether it can execute, and invokes the check only when a requery is needed.
Now, a lot of code (part of our in-house framework) to do this:
I use DelegateCommand from Prism, that looks like this:
/// <summary>
/// This class allows delegating the commanding logic to methods passed as parameters,
/// and enables a View to bind commands to objects that are not part of the element tree.
/// </summary>
public class DelegateCommand : ICommand
{
#region Constructors
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action executeMethod)
: this(executeMethod, null, false)
{
}
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod)
: this(executeMethod, canExecuteMethod, false)
{
}
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod, bool isAutomaticRequeryDisabled)
{
if (executeMethod == null)
{
throw new ArgumentNullException("executeMethod");
}
_executeMethod = executeMethod;
_canExecuteMethod = canExecuteMethod;
_isAutomaticRequeryDisabled = isAutomaticRequeryDisabled;
this.RaiseCanExecuteChanged();
}
#endregion
#region Public Methods
/// <summary>
/// Method to determine if the command can be executed
/// </summary>
public bool CanExecute()
{
if (_canExecuteMethod != null)
{
return _canExecuteMethod();
}
return true;
}
/// <summary>
/// Execution of the command
/// </summary>
public void Execute()
{
if (_executeMethod != null)
{
_executeMethod();
}
}
/// <summary>
/// Property to enable or disable CommandManager's automatic requery on this command
/// </summary>
public bool IsAutomaticRequeryDisabled
{
get
{
return _isAutomaticRequeryDisabled;
}
set
{
if (_isAutomaticRequeryDisabled != value)
{
if (value)
{
CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers);
}
else
{
CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers);
}
_isAutomaticRequeryDisabled = value;
}
}
}
/// <summary>
/// Raises the CanExecuteChaged event
/// </summary>
public void RaiseCanExecuteChanged()
{
OnCanExecuteChanged();
}
/// <summary>
/// Protected virtual method to raise CanExecuteChanged event
/// </summary>
protected virtual void OnCanExecuteChanged()
{
CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers);
}
#endregion
#region ICommand Members
/// <summary>
/// ICommand.CanExecuteChanged implementation
/// </summary>
public event EventHandler CanExecuteChanged
{
add
{
if (!_isAutomaticRequeryDisabled)
{
CommandManager.RequerySuggested += value;
}
CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, 2);
}
remove
{
if (!_isAutomaticRequeryDisabled)
{
CommandManager.RequerySuggested -= value;
}
CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value);
}
}
bool ICommand.CanExecute(object parameter)
{
return CanExecute();
}
void ICommand.Execute(object parameter)
{
Execute();
}
#endregion
#region Data
private readonly Action _executeMethod = null;
private readonly Func<bool> _canExecuteMethod = null;
private bool _isAutomaticRequeryDisabled = false;
private List<WeakReference> _canExecuteChangedHandlers;
#endregion
}
/// <summary>
/// This class allows delegating the commanding logic to methods passed as parameters,
/// and enables a View to bind commands to objects that are not part of the element tree.
/// </summary>
/// <typeparam name="T">Type of the parameter passed to the delegates</typeparam>
public class DelegateCommand<T> : ICommand
{
#region Constructors
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action<T> executeMethod)
: this(executeMethod, null, false)
{
}
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod)
: this(executeMethod, canExecuteMethod, false)
{
}
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod, bool isAutomaticRequeryDisabled)
{
if (executeMethod == null)
{
throw new ArgumentNullException("executeMethod");
}
_executeMethod = executeMethod;
_canExecuteMethod = canExecuteMethod;
_isAutomaticRequeryDisabled = isAutomaticRequeryDisabled;
}
#endregion
#region Public Methods
/// <summary>
/// Method to determine if the command can be executed
/// </summary>
public bool CanExecute(T parameter)
{
if (_canExecuteMethod != null)
{
return _canExecuteMethod(parameter);
}
return true;
}
/// <summary>
/// Execution of the command
/// </summary>
public void Execute(T parameter)
{
if (_executeMethod != null)
{
_executeMethod(parameter);
}
}
/// <summary>
/// Raises the CanExecuteChaged event
/// </summary>
public void RaiseCanExecuteChanged()
{
OnCanExecuteChanged();
}
/// <summary>
/// Protected virtual method to raise CanExecuteChanged event
/// </summary>
protected virtual void OnCanExecuteChanged()
{
CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers);
}
/// <summary>
/// Property to enable or disable CommandManager's automatic requery on this command
/// </summary>
public bool IsAutomaticRequeryDisabled
{
get
{
return _isAutomaticRequeryDisabled;
}
set
{
if (_isAutomaticRequeryDisabled != value)
{
if (value)
{
CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers);
}
else
{
CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers);
}
_isAutomaticRequeryDisabled = value;
}
}
}
#endregion
#region ICommand Members
/// <summary>
/// ICommand.CanExecuteChanged implementation
/// </summary>
public event EventHandler CanExecuteChanged
{
add
{
if (!_isAutomaticRequeryDisabled)
{
CommandManager.RequerySuggested += value;
}
CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, 2);
}
remove
{
if (!_isAutomaticRequeryDisabled)
{
CommandManager.RequerySuggested -= value;
}
CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value);
}
}
bool ICommand.CanExecute(object parameter)
{
// if T is of value type and the parameter is not
// set yet, then return false if CanExecute delegate
// exists, else return true
if (parameter == null &&
typeof(T).IsValueType)
{
return (_canExecuteMethod == null);
}
return CanExecute((T)parameter);
}
void ICommand.Execute(object parameter)
{
Execute((T)parameter);
}
#endregion
#region Data
private readonly Action<T> _executeMethod = null;
private readonly Func<T, bool> _canExecuteMethod = null;
private bool _isAutomaticRequeryDisabled = false;
private List<WeakReference> _canExecuteChangedHandlers;
#endregion
}
/// <summary>
/// This class contains methods for the CommandManager that help avoid memory leaks by
/// using weak references.
/// </summary>
internal class CommandManagerHelper
{
internal static void CallWeakReferenceHandlers(List<WeakReference> handlers)
{
if (handlers != null)
{
// Take a snapshot of the handlers before we call out to them since the handlers
// could cause the array to me modified while we are reading it.
EventHandler[] callees = new EventHandler[handlers.Count];
int count = 0;
for (int i = handlers.Count - 1; i >= 0; i--)
{
WeakReference reference = handlers[i];
EventHandler handler = reference.Target as EventHandler;
if (handler == null)
{
// Clean up old handlers that have been collected
handlers.RemoveAt(i);
}
else
{
callees[count] = handler;
count++;
}
}
// Call the handlers that we snapshotted
for (int i = 0; i < count; i++)
{
EventHandler handler = callees[i];
handler(null, EventArgs.Empty);
}
}
}
internal static void AddHandlersToRequerySuggested(List<WeakReference> handlers)
{
if (handlers != null)
{
foreach (WeakReference handlerRef in handlers)
{
EventHandler handler = handlerRef.Target as EventHandler;
if (handler != null)
{
CommandManager.RequerySuggested += handler;
}
}
}
}
internal static void RemoveHandlersFromRequerySuggested(List<WeakReference> handlers)
{
if (handlers != null)
{
foreach (WeakReference handlerRef in handlers)
{
EventHandler handler = handlerRef.Target as EventHandler;
if (handler != null)
{
CommandManager.RequerySuggested -= handler;
}
}
}
}
internal static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler)
{
AddWeakReferenceHandler(ref handlers, handler, -1);
}
internal static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler, int defaultListSize)
{
if (handlers == null)
{
handlers = (defaultListSize > 0 ? new List<WeakReference>(defaultListSize) : new List<WeakReference>());
}
handlers.Add(new WeakReference(handler));
}
internal static void RemoveWeakReferenceHandler(List<WeakReference> handlers, EventHandler handler)
{
if (handlers != null)
{
for (int i = handlers.Count - 1; i >= 0; i--)
{
WeakReference reference = handlers[i];
EventHandler existingHandler = reference.Target as EventHandler;
if ((existingHandler == null) || (existingHandler == handler))
{
// Clean up old handlers that have been collected
// in addition to the handler that is to be removed.
handlers.RemoveAt(i);
}
}
}
}
}
I have then written a ListenOn extension method, that 'binds' the command to a property, and invokes its RaiseCanExecuteChanged:
public static class DelegateCommandExtensions
{
/// <summary>
/// Makes DelegateCommnand listen on PropertyChanged events of some object,
/// so that DelegateCommnand can update its IsEnabled property.
/// </summary>
public static DelegateCommand ListenOn<ObservedType, PropertyType>
(this DelegateCommand delegateCommand,
ObservedType observedObject,
Expression<Func<ObservedType, PropertyType>> propertyExpression,
Dispatcher dispatcher)
where ObservedType : INotifyPropertyChanged
{
//string propertyName = observedObject.GetPropertyName(propertyExpression);
string propertyName = NotifyPropertyChangedBaseExtensions.GetPropertyName(propertyExpression);
observedObject.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == propertyName)
{
if (dispatcher != null)
{
ThreadTools.RunInDispatcher(dispatcher, delegateCommand.RaiseCanExecuteChanged);
}
else
{
delegateCommand.RaiseCanExecuteChanged();
}
}
};
return delegateCommand; //chain calling
}
/// <summary>
/// Makes DelegateCommnand listen on PropertyChanged events of some object,
/// so that DelegateCommnand can update its IsEnabled property.
/// </summary>
public static DelegateCommand<T> ListenOn<T, ObservedType, PropertyType>
(this DelegateCommand<T> delegateCommand,
ObservedType observedObject,
Expression<Func<ObservedType, PropertyType>> propertyExpression,
Dispatcher dispatcher)
where ObservedType : INotifyPropertyChanged
{
//string propertyName = observedObject.GetPropertyName(propertyExpression);
string propertyName = NotifyPropertyChangedBaseExtensions.GetPropertyName(propertyExpression);
observedObject.PropertyChanged += (object sender, PropertyChangedEventArgs e) =>
{
if (e.PropertyName == propertyName)
{
if (dispatcher != null)
{
ThreadTools.RunInDispatcher(dispatcher, delegateCommand.RaiseCanExecuteChanged);
}
else
{
delegateCommand.RaiseCanExecuteChanged();
}
}
};
return delegateCommand; //chain calling
}
}
You then need the following extension to NotifyPropertyChanged
/// <summary>
/// <see cref="http://dotnet.dzone.com/news/silverlightwpf-implementing"/>
/// </summary>
public static class NotifyPropertyChangedBaseExtensions
{
/// <summary>
/// Raises PropertyChanged event.
/// To use: call the extension method with this: this.OnPropertyChanged(n => n.Title);
/// </summary>
/// <typeparam name="T">Property owner</typeparam>
/// <typeparam name="TProperty">Type of property</typeparam>
/// <param name="observableBase"></param>
/// <param name="expression">Property expression like 'n => n.Property'</param>
public static void OnPropertyChanged<T, TProperty>(this T observableBase, Expression<Func<T, TProperty>> expression) where T : INotifyPropertyChangedWithRaise
{
observableBase.OnPropertyChanged(GetPropertyName<T, TProperty>(expression));
}
public static string GetPropertyName<T, TProperty>(Expression<Func<T, TProperty>> expression) where T : INotifyPropertyChanged
{
if (expression == null)
throw new ArgumentNullException("expression");
var lambda = expression as LambdaExpression;
MemberExpression memberExpression;
if (lambda.Body is UnaryExpression)
{
var unaryExpression = lambda.Body as UnaryExpression;
memberExpression = unaryExpression.Operand as MemberExpression;
}
else
{
memberExpression = lambda.Body as MemberExpression;
}
if (memberExpression == null)
throw new ArgumentException("Please provide a lambda expression like 'n => n.PropertyName'");
MemberInfo memberInfo = memberExpression.Member;
if (String.IsNullOrEmpty(memberInfo.Name))
throw new ArgumentException("'expression' did not provide a property name.");
return memberInfo.Name;
}
}
where INotifyPropertyChangedWithRaise is this (it estabilishes standard interface for raising NotifyPropertyChanged events):
public interface INotifyPropertyChangedWithRaise : INotifyPropertyChanged
{
void OnPropertyChanged(string propertyName);
}
Last piece of puzzle is this:
public class ThreadTools
{
public static void RunInDispatcher(Dispatcher dispatcher, Action action)
{
RunInDispatcher(dispatcher, DispatcherPriority.Normal, action);
}
public static void RunInDispatcher(Dispatcher dispatcher, DispatcherPriority priority, Action action)
{
if (action == null) { return; }
if (dispatcher.CheckAccess())
{
// we are already on thread associated with the dispatcher -> just call action
try
{
action();
}
catch (Exception ex)
{
//Log error here!
}
}
else
{
// we are on different thread, invoke action on dispatcher's thread
dispatcher.BeginInvoke(
priority,
(Action)(
() =>
{
try
{
action();
}
catch (Exception ex)
{
//Log error here!
}
})
);
}
}
}
This solution is a reduced version of the solution proposed by Tomáš Kafka(thanks to Tomas for describing his solution in detail)in this thread.
In Tomas's solution he had
1) DelegateCommand
2) CommandManagerHelper
3) DelegateCommandExtensions
4) NotifyPropertyChangedBaseExtensions
5) INotifyPropertyChangedWithRaise
6) ThreadTools
This solution has
1) DelegateCommand
2) DelegateCommandExtensions method and NotifyPropertyChangedBaseExtensions method in Delegate Command itself.
Note Since our wpf application follows MVVM pattern and we handle commands at viewmodel level which executes in UI thread we don't need to get the reference to UI disptacher.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
using System.Windows.Input;
namespace ExampleForDelegateCommand
{
public class DelegateCommand : ICommand
{
public Predicate<object> CanExecuteDelegate { get; set; }
private List<INotifyPropertyChanged> propertiesToListenTo;
private List<WeakReference> ControlEvent;
public DelegateCommand()
{
ControlEvent= new List<WeakReference>();
}
public List<INotifyPropertyChanged> PropertiesToListenTo
{
get { return propertiesToListenTo; }
set
{
propertiesToListenTo = value;
}
}
private Action<object> executeDelegate;
public Action<object> ExecuteDelegate
{
get { return executeDelegate; }
set
{
executeDelegate = value;
ListenForNotificationFrom((INotifyPropertyChanged)executeDelegate.Target);
}
}
public static ICommand Create(Action<object> exec)
{
return new SimpleCommand { ExecuteDelegate = exec };
}
#region ICommand Members
public bool CanExecute(object parameter)
{
if (CanExecuteDelegate != null)
return CanExecuteDelegate(parameter);
return true; // if there is no can execute default to true
}
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
ControlEvent.Add(new WeakReference(value));
}
remove
{
CommandManager.RequerySuggested -= value;
ControlEvent.Remove(ControlEvent.Find(r => ((EventHandler) r.Target) == value));
}
}
public void Execute(object parameter)
{
if (ExecuteDelegate != null)
ExecuteDelegate(parameter);
}
#endregion
public void RaiseCanExecuteChanged()
{
if (ControlEvent != null && ControlEvent.Count > 0)
{
ControlEvent.ForEach(ce =>
{
if(ce.Target!=null)
((EventHandler) (ce.Target)).Invoke(null, EventArgs.Empty);
});
}
}
public DelegateCommand ListenOn<TObservedType, TPropertyType>(TObservedType viewModel, Expression<Func<TObservedType, TPropertyType>> propertyExpression) where TObservedType : INotifyPropertyChanged
{
string propertyName = GetPropertyName(propertyExpression);
viewModel.PropertyChanged += (PropertyChangedEventHandler)((sender, e) =>
{
if (e.PropertyName == propertyName) RaiseCanExecuteChanged();
});
return this;
}
public void ListenForNotificationFrom<TObservedType>(TObservedType viewModel) where TObservedType : INotifyPropertyChanged
{
viewModel.PropertyChanged += (PropertyChangedEventHandler)((sender, e) =>
{
RaiseCanExecuteChanged();
});
}
private string GetPropertyName<T, TProperty>(Expression<Func<T, TProperty>> expression) where T : INotifyPropertyChanged
{
var lambda = expression as LambdaExpression;
MemberInfo memberInfo = GetmemberExpression(lambda).Member;
return memberInfo.Name;
}
private MemberExpression GetmemberExpression(LambdaExpression lambda)
{
MemberExpression memberExpression;
if (lambda.Body is UnaryExpression)
{
var unaryExpression = lambda.Body as UnaryExpression;
memberExpression = unaryExpression.Operand as MemberExpression;
}
else
memberExpression = lambda.Body as MemberExpression;
return memberExpression;
}
}}
Explanation of the solution:
Normally when we bind a UI element(Button)to the ICommand implementation the WPF Button registers for a Event "CanExecuteChanged" in ICommand implementation .If your Icommand implementation for "CanExecuteChanged" hook to the CommandManager's RequesySuggest event(read this article http://joshsmithonwpf.wordpress.com/2008/06/17/allowing-commandmanager-to-query-your-icommand-objects/) then when ever CommandManager detects conditions that might change the ability of a command to execute(changes like Focus shifts and some keyboard events) , CommandManager’s RequerySuggested event occurs which in turn will cause Button'e delegate to be called since we hooked the buttos's delgate to CommandManager’s RequerySuggested in the implementation of "CanExecuteChanged" in our DelegateCommand .
But the problem is that ComandManager is not able to always detect the changes. Hence the solution it to raise "CanExecuteChanged" when our command implementation(DelegateCommand) detects there is a change.Normally when we declare the delagate for ICommand's CanExecute in our viewmodel we bind to properties declared in our viewmodel and our ICommand implementation can listen for "propertychanged" events on these properties. Thats what the "ListenForNotificationFrom" method of the DelegateCommand does. In case the client code does not register for specific property changes the DelegateCommand by defaults listens to any property change on the view model where command is declared and defined.
"ControlEvent" in DelegateCommand which is list of EventHandler that stores the Button's
"CanExecuteChange EventHandler" is declared as weak reference to avoid memory leaks.
How will ViewModel use this DelegateCommand
There are 2 ways to use it.
(the second usage is more specific to the properties that you want the Command to listen to.
delegateCommand = new DelegateCommand
{
ExecuteDelegate = Search,
CanExecuteDelegate = (r) => !IsBusy
};
anotherDelegateCommand = new DelegateCommand
{
ExecuteDelegate = SearchOne,
CanExecuteDelegate = (r) => !IsBusyOne
}.ListenOn(this, n => n.IsBusyOne);
A detailed ViewModel
public class ExampleViewModel
{
public SearchViewModelBase()
{
delegateCommand = new DelegateCommand
{
ExecuteDelegate = Search,
CanExecuteDelegate = (r) => !IsBusy
};
anotherDelegateCommand = new DelegateCommand
{
ExecuteDelegate = SearchOne,
CanExecuteDelegate = (r) => !IsBusyOne
}.ListenOn(this, n => n.IsBusyOne);
}
private bool isBusy;
public virtual bool IsBusy
{
get { return isBusy; }
set
{
if (isBusy == value) return;
isBusy = value;
NotifyPropertyChanged(MethodBase.GetCurrentMethod());
}
}
private bool isBusyOne;
public virtual bool IsBusyOne
{
get { return isBusyOne; }
set
{
if (isBusyOne == value) return;
isBusyOne = value;
NotifyPropertyChanged(MethodBase.GetCurrentMethod());
}
}
private void Search(object obj)
{
IsBusy = true;
new SearchService().Search(Callback);
}
public void Callback(ServiceResponse response)
{
IsBusy = false;
}
private void Search(object obj)
{
IsBusyOne = true;
new SearchService().Search(CallbackOne);
}
public void CallbackOne(ServiceResponse response)
{
IsBusyOne = false;
}
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private void NotifyPropertyChanged(MethodBase methodBase)
{
string methodName = methodBase.Name;
if (!methodName.StartsWith("set_"))
{
var ex = new ArgumentException("MethodBase must refer to a Property Setter method.");
throw ex;
}
NotifyPropertyChanged(methodName.Substring(4));
}
}
Tomas has a nice solution, but pls note there's a serious bug in that the CanExecute will not always fire when bound to a Button due to this :
// Call the handlers that we snapshotted
for (int i = 0; i < count; i++)
{
EventHandler handler = callees[i];
handler(null, EventArgs.Empty);
}
The 'null' parameter passed in causes issues with the CanExecuteChangedEventManager (used by the WPF Button class to listen to changes on any Command bound to it). Specifically, the CanExecuteChangedEventManager maintains a collection of weak events that need to be invoked to determine if the command Can-Execute() but this collection is keyed by the 'sender'.
The fix is simple and works for me - change the signature to
internal static void CallWeakReferenceHandlers(ICommand sender, List<WeakReference> handlers)
{
....
handler(sender, EventArgs.Empty);
}
Sorry I haven't described it too well - in a bit of a rush to catch up with my dev now after taking a few hours to figure this out !
I would suggest looking into ReactiveUI and specifically at the ICommand implementation it provides, ReactiveCommand. It uses a different approach than DelegateCommand/RelayCommand which are implemented with delegates for CanExecute that must be actively evaluated. ReactiveCommand's value for CanExecute is pushed using IObservables.
is there a way I can make CommandManager.InvalidateRequerySuggested() do it's job faster?
Yes, there is way to make it work faster!
Implement Command to keep / cache CanExecuteState in a boolean variable.
Implement RaiseCanExecuteChanged method to recalculate CanExecuteState and if it really changed to raise CanExecuteChanged event.
Implement CanExecute method to simply return CanExecuteState.
When InvalidateRequerySuggested method is invoked Command subscribers will only read CanExecuteState variable by invoking CanExecute method and check if it changed or not. That's almost zero overhead. All Commands will be disabled / enabled almost the same time.
All work will be done in RaiseCanExecuteChanged method that will be called only once for a Command and only for a limited set of Commands.
Try writing your own binding that calls your RaiseCanExecuteChanged() within converts? it is easier
Just to clarify:
You want to fire an update of CanExecute when Command property changed
Create your own binding class that detect changes in the Command property and then calls RaiseCanExecuteChanged()
Use this binding in CommandParameter
Worked for me.

Resources