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
' 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 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 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 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 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.
发表评论
BAG7Eh Really informative article post. Keep writing.
aXsKSW F*ckin' remarkable issues here. I'm very glad to see your article. Thank you a lot and i am having a look ahead to contact you. Will you kindly drop me a mail?
JE11Bd Fantastic blog article.Thanks Again. Fantastic.
a22O2Z Enjoyed every bit of your blog article.Thanks Again. Will read on...
XnLWC1 Very good article post.Really thank you! Will read on...
hgqqtJ Hey, thanks for the post.Really looking forward to read more.
JkmM5A Looking forward to reading more. Great blog post.Thanks Again.
UioiUl Thanks for sharing, this is a fantastic blog post.Much thanks again. Fantastic.
WrA7RI I really like and appreciate your blog post.Really looking forward to read more.
1o3lE8 I really like and appreciate your post. Keep writing.
hTrJKq Thanks a lot for the blog.Thanks Again. Awesome.