Like lots of people working with WPF, I've been writing my own MVVM framework. I started using this in an application I was writing. One of the things it needed to do was obtain the dimensions of a Canvas
object. As such, a subscription to the SizeChanged
event was used. The connection was formed using DataBinding
to my implementation of an event-to-command mapper.
The code below are the classes from the MVVM framework plus a sample application that demonstrates the problem. This is just a button within a Canvas
that when pressed pops up a dialog displaying the Canvas
' dimensions.
<Window x:Class="SizeChangedEventTest2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
xmlns:mvvm="clr-namespace:PABLib.MVVM;assembly=PABLib.MVVM"
xmlns:local="clr-namespace:SizeChangedEventTest2">
<Canvas mvvm:EventCommand.Name="SizeChanged"
mvvm:EventCommand.Command="{Binding SizeChanged}">
<Button Content="Hello" Command="{Binding PressMe}"/>
</Canvas>
</Window>
The code below shows my basic implementation of the command-to-event pattern. I would have left it out but seeing how it's used is crucial to the explanation of the problem and the solution. Please note that EventCommand
is actually in the PABLib.MVVM
namespace as referred to in the XAML above but I've left it out of the C# code to save space.
public class EventCommand
{
public static DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command",
typeof(ICommand),
typeof(EventCommand));
public static void SetCommand(DependencyObject target, ICommand value)
{
target.SetValue(EventCommand.CommandProperty, value);
}
public static ICommand GetCommand(DependencyObject target)
{
return (ICommand)target.GetValue(CommandProperty);
}
public static DependencyProperty EventNameProperty =
DependencyProperty.RegisterAttached("Name",
typeof(string),
typeof(EventCommand),
new FrameworkPropertyMetadata(NameChanged));
public static void SetName(DependencyObject target, string value)
{
target.SetValue(EventCommand.EventNameProperty, value);
}
public static string GetName(DependencyObject target)
{
return (string)target.GetValue(EventNameProperty);
}
private static void NameChanged
(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
UIElement element = target as UIElement;
if (element != null)
{
if ((e.NewValue != null) && (e.OldValue == null))
{
EventInfo eventInfo = element.GetType().GetEvent((string)e.NewValue);
Delegate d = Delegate.CreateDelegate(eventInfo.EventHandlerType,
typeof(EventCommand).GetMethod("Handler",
BindingFlags.NonPublic | BindingFlags.Static));
eventInfo.AddEventHandler(element, d);
}
else if ((e.NewValue == null) && (e.OldValue != null))
{
EventInfo eventInfo = element.GetType().GetEvent((string)e.OldValue);
Delegate d = Delegate.CreateDelegate(eventInfo.EventHandlerType,
typeof(EventCommand).GetMethod("Handler"));
eventInfo.RemoveEventHandler(element, d);
}
}
}
static void Handler(object sender, EventArgs e)
{
UIElement element = (UIElement)sender;
ICommand command = (ICommand)element.GetValue(EventCommand.CommandProperty);
var src = Tuple.Create(sender, e);
if (command != null && command.CanExecute(src) == true)
command.Execute(src);
}
}
The bindings used in XAML refer to properties in Window
's ViewModel
. This is defined as follows:
class MainWindowViewModel
{
public ICommand PressMe { get; private set; }
public ICommand SizeChanged { get; private set; }
private int m_width = 0;
private int m_height = 0;
public MainWindowViewModel()
{
SizeChanged = new PABLib.MVVM.RelayCommand<object>((x) =>
{
SizeChangedEventArgs args =
(SizeChangedEventArgs)((Tuple<object, EventArgs>)x).Item2;
m_width = (int)args.NewSize.Width;
m_height = (int)args.NewSize.Height;
});
PressMe = new PABLib.MVVM.RelayCommand<object>((x) =>
{
MessageBox.Show(string.Format("Width:{0}, Height:{1}", m_width, m_height));
});
}
}
For the sake of completeness, here is the implementation of RelayCommand
. This is pretty much the basic version as originally created by Josh Smith.
public class RelayCommand<T> : ICommand
{
Action<T> _Execute { get; set; }
Predicate<T> _CanExecute { get; set; }
public RelayCommand(Action<T> execute, Predicate<T> canExecute = null)
{
_Execute = execute;
_CanExecute = canExecute;
}
public bool CanExecute(object parameter)
{
if (_CanExecute == null)
return true;
else
return _CanExecute((T)parameter);
}
public void Execute(object parameter)
{
if (_Execute != null)
_Execute((T)parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
However, rather than just obtaining the dimensions when changed, these were also required when the Canvas
was first shown. The problem was that when using my MVVM framework, it was only capturing events if the window was resized but not the initial sizing event. For the sample app, this meant pressing the button the first time yielded results of 0 for both width and height. I switched back to a conventional code-behind page approach as a sanity check. This worked!
At this point, I started debugging the code more and discovered that the initial SizeChanged
event was being fired and handled by the EventCommand
code. However, when it came to invoke the ICommand
associated with the EventCommand
, this was null
(in the Handler
method of EventCommand
). The strange thing here was that the event name had been successfully passed to EventCommand
but the command hadn't. Both of these are stored as Attached Properties (as is normal for event-to-command implementations).
The difference between the event name and the command is that the event name was a hard-coded string
in the XAML whereas the command was being obtained using data binding to the main window's ViewModel
. Therefore the culprit appeared to be that the binding hadn't executed. There was no problem with the validity of the binding as all the SizeChanged
events bar the initial were being received, and in debug mode, VS was not reporting any issues with the binding.
The only thing I could think of is that the initial event was being fired before the binding had been processed. This was confirmed by extending the Attached Property definition for the CommandProperty
to include a CommandChanged
callback, for example:
public static DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command",
typeof(ICommand),
typeof(EventCommand),
new FrameworkPropertyMetadata(CommandChanged));
private static void CommandChanged(DependencyObject target,
DependencyPropertyChangedEventArgs e)
{
}
A break point set on CommandChanged
showed this wasn't invoked until after the event had fired, confirming that the binding hadn't occurred.
The way the ViewModel
was set as the Data Context for the main window was by removing the StartupUri
element from the Application
element in App.xaml.cs, e.g.:
<Application x:Class="SizeChangedEventTest2.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
</Application.Resources>
</Application>
and modifying App.xaml.cs to be:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
MainWindowViewModel vm = new MainWindowViewModel();
MainWindow win = new MainWindow();
win.DataContext = vm;
this.MainWindow = win;
this.MainWindow.Show();
}
}
After some searching, I noticed other projects setting the DataContext
of the main window (to the ViewModel
) in different ways. This got me to thinking that perhaps the DataContext
was being established too late.
To address this, App.xaml and App.xaml.cs were put back to their initial states, and instead the ViewModel
created and attached in the constructor for MainWindow
, for example:
public partial class MainWindow : Window
{
public MainWindow()
{
this.DataContext = new MainWindowViewModel();
InitializeComponent();
}
}
This fixed the problem! As an experiment, InitializeComponent()
was moved to the top of the constructor. It stopped working. I didn't particularly like creating the ViewModel
here so this code was removed, and instead it was created in XAML as follows:
<Window x:Class="SizeChangedEventTest2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
xmlns:mvvm="clr-namespace:PABLib.MVVM;assembly=PABLib.MVVM"
xmlns:local="clr-namespace:SizeChangedEventTest2">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Canvas mvvm:EventCommand.Name="SizeChanged"
mvvm:EventCommand.Command="{Binding SizeChanged}">
<Button Content="Hello" Command="{Binding PressMe}"/>
</Canvas>
</Window>
This too worked. This is where I'm currently at. From this, I conclude that it is critically important to make sure that a View
's DataContext
is properly created and attached before the underlying Window
is displayed, otherwise initial events will be missed.