When does the ui detach from commands?

2019-01-26 12:40发布

I'm really scratching my head with this one. I have a mainwindow which opens a dialog. After the dialog closes, the CanExecute method on commands bound in the dialog are still executing. This is causing some serious problems in my application.

Example:

MainWindow has a button with a click handler. This is the click event handler:

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        DialogWindow window = new DialogWindow();
        window.ShowDialog();
    }

In the dialog I bind an items control to a static resource in the dialog window, and each item in the list has a command:

<Window.Resources>

    <Collections:ArrayList x:Key="itemsSource">
        <local:ItemViewModel Description="A"></local:ItemViewModel>
        <local:ItemViewModel Description="B"></local:ItemViewModel>
        <local:ItemViewModel Description="C"></local:ItemViewModel>
    </Collections:ArrayList>

    <DataTemplate DataType="{x:Type local:ItemViewModel}">
            <Button Grid.Column="1" Command="{Binding Path=CommandClickMe}" Content="{Binding Path=Description}" Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}">
            </Button>
    </DataTemplate>

</Window.Resources>

<Grid>
    <ToolBar ItemsSource="{StaticResource itemsSource}"></ToolBar>
</Grid>

This is the viewmodel:

public class ItemViewModel
{
    private RelayWpfCommand<object> _commandClickMe;

    public RelayWpfCommand<object> CommandClickMe
    {
        get
        {
            if (_commandClickMe == null)
                _commandClickMe = new RelayWpfCommand<object>(obj => System.Console.Out.WriteLine("Hei mom"), obj => CanClickMe());

            return _commandClickMe;
        }
    }

    private bool CanClickMe()
    {
        return true;
    }

    public string Description { get; set; }

And this is the DelegateCommand implementation:

public class RelayWpfCommand<T> : ICommand
{
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    private readonly Predicate<T> _canExecute;
    private readonly Action<T> _execute;

    public RelayWpfCommand(Action<T> execute, Predicate<T> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    /// <summary>
    /// Forces a notification that the CanExecute state has changed
    /// </summary>
    public void RaiseCanExecuteChanged()
    {
        CommandManager.InvalidateRequerySuggested();
    }

    public bool CanExecute(T parameter)
    {
        return _canExecute(parameter);
    }

    public void Execute(T parameter)
    {
        _execute(parameter);
    }

    bool ICommand.CanExecute(object parameter)
    {
        if (!IsParameterValidType(parameter))
            return false;

        return CanExecute((T)parameter);
    }

    void ICommand.Execute(object parameter)
    {
        if (!IsParameterValidType(parameter))
            throw new ArgumentException(string.Format("Parameter must be of type {0}", typeof(T)));

        Execute((T)parameter);
    }

    private static bool IsParameterValidType(object parameter)
    {
        if (parameter != null && !typeof(T).IsAssignableFrom(parameter.GetType()))
            return false;

        return true;
    }
}

Now, If I close the dialog window and set a breakpoint in the CanExecute (I'm using Prism DelegateCommand with weak event subscription) method on the viewmodel, I notice that it triggers although the dialog has been closed. Why on earth is the binding between the button in the dialog and the command on the ViewModel still alive?

And I am checking if its being executed by closing the window and at a later time setting a breakpoint in the "CanClickMe" method in the viewmodel. It will get executed for a while, then suddenly stop (probably due to GC). This non-determenistic behaviour is causing problems because in the real application the viewmodel might already bee disposed.

4条回答
forever°为你锁心
2楼-- · 2019-01-26 13:11

I've seen this catch many times in different projects, I'm not sure whether this creepy bug lurks in your app too, but it's worth checking.

There is a known memory leak issue in WPF 3.5 (including SP1), basically you can encounter it if you are binding to something that isn’t a DependencyProperty or doesn’t implement INotifyPropertyChanged. And this is exactly what your code is about.

Just implement INotifyPropertyChanged on ItemViewModel and see how it goes. Hope this helps.

查看更多
We Are One
3楼-- · 2019-01-26 13:21

You may use the WeakEvent Pattern to mitigate this problem. Please refer to the following Stackoverflow question: Is Josh Smith's implementation of the RelayCommand flawed?

查看更多
甜甜的少女心
4楼-- · 2019-01-26 13:24

You could clear the CommandBindings Collection of your window, when it closes.

查看更多
放我归山
5楼-- · 2019-01-26 13:25

rather than having your command as a property, could you try the following:

public ICommand CommandClickMe
{
   get
   {
       return new RelayWpfCommand<object>((obj)=>System.Console.Out.WriteLine("Hei mom"), obj => CanClickMe());
   }
}
查看更多
登录 后发表回答