views:

271

answers:

1

I'm trying to use the MVVM pattern in my Silverlight 3 application and am having problems getting binding to a command property of a view model working. First off, I'm trying to add an attached property called ClickCommand, like this:

public static class Command
{
    public static readonly DependencyProperty ClickCommandProperty = 
        DependencyProperty.RegisterAttached(
            "ClickCommand", typeof(Command<RoutedEventHandler>), 
            typeof(Command), null);

    public static Command<RoutedEventHandler> GetClickCommand(
        DependencyObject target)
    {
        return target.GetValue(ClickCommandProperty) 
            as Command<RoutedEventHandler>;
    }

    public static void SetClickCommand(
        DependencyObject target, Command<RoutedEventHandler> value)
    {
        // Breakpoints here are never reached
        var btn = target as ButtonBase;
        if (btn != null)
        {
            var oldValue = GetClickCommand(target);
            btn.Click -= oldValue.Action;

            target.SetValue(ClickCommandProperty, value);
            btn.Click += value.Action;
        }
    }
}

The generic Command class is a wrapper around a delegate. I'm only wrapping a delegate because I wondered if having a delegate type for a property was the reason things weren't working for me originally. Here's that class:

public class Command<T> /* I'm not allowed to constrain T to a delegate type */
{
    public Command(T action)
    {
        this.Action = action;
    }

    public T Action { get; set; }
}

Here's how I am using the attached property:

<Button u:Command.ClickCommand="{Binding DoThatThing}" Content="New"/>

The syntax seems to be accepted, and I think that when I tested all of this with a string property type, that worked fine. Here's the view model class that is being bound to:

public class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    public Command<RoutedEventHandler> DoThatThing
    {
        get
        {
            return new Command<RoutedEventHandler>(
                (s, e) => Debug.WriteLine("Never output!"));
        }
    }
}

The delegate contained in the Command property is never invoked. Also, when I place breakpoints in the getter and setter of the attached property, they are never reached.

In trying to isolate the problem, I changing the property type to string; the breakpoint in the getter and setter was also never reached, yet throwing an exception in them did cause the application to terminate, so I am thinking it's a framework eccentricity.

Why is this stuff not working? I also welcome alternate, hopefully simpler ways to bind event handlers to view models.

+3  A: 

You have at least two problems here.

First, you are relying on the SetXxx method being executed. The CLR wrappers for dependency properties (the property setter or SetXxx method) are not executed when the DP is set from XAML; rather, WPF sets the value of the internally managed DP "slot" directly. (This also explains why your breakpoints were never hit.) Therefore, your logic for handling changes must always occur in the OnXxxChanged callback, not in the setter; WPF will call that callback for you when the property changes regardless of where that change comes from. Thus (example taken from a slightly different implementation of commands, but should give you the idea):

// Note callback in PropertyMetadata

public static readonly DependencyProperty CommandProperty =
  DependencyProperty.RegisterAttached("Command", typeof(ICommand), typeof(Click),
  new PropertyMetadata(OnCommandChanged));

// GetXxx and SetXxx wrappers contain boilerplate only

public static ICommand GetCommand(DependencyObject obj)
{
  return (ICommand)obj.GetValue(CommandProperty);
}

public static void SetCommand(DependencyObject obj, ICommand value)
{
  obj.SetValue(CommandProperty, value);
}

// WPF will call the following when the property is set, even when it's set in XAML

private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  ButtonBase button = d as ButtonBase;
  if (button != null)
  {
    // do something with button.Click here
  }
}

Second, even with this change, setting ClickCommand on a control that doesn't already have a value set will cause an exception, because oldValue is null and therefore oldValue.Action causes a NullReferenceException. You need to check for this case (you should also check for newValue == null though this is unlikely ever to happen).

itowlson
Thank you very much. Doing the changed handler fixed the problem.
Jacob