Sunday, 12 June 2011

Getting WPF SizeChanged Events at start-up when using MVVM and DataContext

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's 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 but 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# 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 we're putting in a new command and there wasn't one already hook the event
   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);
   }
   // If we're clearing the command and it wasn't already null unhook the event
   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 the 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 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 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 an 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 an CommandChanged callback e,.g.

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, e.g.

public partial class MainWindow : Window
{
 public MainWindow()
 {
  this.DataContext = new MainWindowViewModel();

  InitializeComponent();
 }
}

This fixed the problem!  As an experiment InitializeComponent() was moved to top of the constructor.  It stopped working. I didn't particularly like creating the ViewModel here so instead 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.

No comments: