Updating a reference to a command of a nested View

2019-04-15 02:44发布

问题:

I know I'm probably missing something simple and obvious, but at the moment it eludes me.

I'm attempting to use the MVVM pattern.

How do you update a reference to a command in a viewmodel that is linked to a child viewmodel?

I've got a view (MainView) bound to a viewmodel (MainViewModel). On MainView, I've got an instance of another view (SummaryView) bound to a viewmodel (SummaryViewModel). SummaryViewModel contains a collection of a third viewmodel (SummaryFilterViewModel).

On SummaryView, there is a TabControl and each tab on it is bound to one of the SummaryFilterViewModel instances in the SummaryViewodel collection.

On MainView there is a button that is bound to a command in MainViewModel.

What I want to happen is for the command logic to live within the SummaryFilterViewModel class. So, whichever tab is currently displayed needs to be wired up to the command that the button on MainView fires.

What I tried to do was this:

  1. The individual SummaryFilterViewModel objects stored in the collection in SummaryViewModel hold the actual implementations of the ShoutCommand.
  2. A CommandReference object in the XAML of MainView binds to a ShoutCommand property of the MainViewModel
  3. The ShoutCommand property of the MainViewModel returns a reference to the ShoutCommand property of the SummaryViewModel object stored in MainViewModel.
  4. The ShoutCommand property of the SummaryViewModel returns a reference to the ShoutCommand property of whichever is the currently selected SummaryFilterViewModel.

What happens, is that the command does not get updated when the user changes tabs.

Am I way off base in how to implement this? Do I need to move the implementation of the command into the SummaryViewModel class?

Thanks in advance for any help!

The source to my solution is listed below:

ViewModels

SummaryView.xaml

<UserControl x:Class="NestedCommands.Views.SummaryView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         mc:Ignorable="d" 
         d:DesignHeight="309" d:DesignWidth="476">
<Grid>
    <TabControl SelectedIndex="{Binding SelectedTabIndex}">
        <TabItem DataContext="{Binding Filters[0]}" Header="{Binding FilterName}">
            <ListBox ItemsSource="{Binding ListData}" />
        </TabItem>
        <TabItem DataContext="{Binding Filters[1]}" Header="{Binding FilterName}">
            <ListBox ItemsSource="{Binding ListData}" />
        </TabItem>
        <TabItem DataContext="{Binding Filters[2]}" Header="{Binding FilterName}">
            <ListBox ItemsSource="{Binding ListData}" />
        </TabItem>
    </TabControl>
</Grid>

MainView.xaml

<Window x:Class="NestedCommands.Views.MainView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:v="clr-namespace:NestedCommands.Views"
    xmlns:c="clr-namespace:NestedCommands.Commands"
    Title="MainView" Height="336" Width="420">
<Window.Resources>
    <c:CommandReference x:Key="ShoutCommandReference" Command="{Binding ShoutCommand}" />
</Window.Resources>
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="1*" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <v:SummaryView Grid.Row="0"
                   DataContext="{Binding SummaryViewModel}" />
    <Button Content="Shout Command"
            Grid.Row="1"
            Command="{StaticResource ShoutCommandReference}" />
</Grid>


Command Classes

CommandReference.cs

using System;
using System.Windows;
using System.Windows.Input;

namespace NestedCommands.Commands
{
    /// <summary>
    /// This class facilitates associating a key binding in XAML markup to a command
    /// defined in a View Model by exposing a Command dependency property.
    /// The class derives from Freezable to work around a limitation in WPF when data-binding from XAML.
    /// </summary>
    public class CommandReference : Freezable, ICommand
    {
        public CommandReference()
        {
            // Blank
        }

        public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(CommandReference), new PropertyMetadata(new PropertyChangedCallback(OnCommandChanged)));

        public ICommand Command
        {
            get { return (ICommand)GetValue(CommandProperty); }
            set { SetValue(CommandProperty, value); }
        }

        #region ICommand Members

        public bool CanExecute(object parameter)
        {
            if (Command != null)
                return Command.CanExecute(parameter);
            return false;
        }

        public void Execute(object parameter)
        {
            Command.Execute(parameter);
        }

        public event EventHandler CanExecuteChanged;

        private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CommandReference commandReference = d as CommandReference;
            ICommand oldCommand = e.OldValue as ICommand;
            ICommand newCommand = e.NewValue as ICommand;

            if (oldCommand != null)
            {
                oldCommand.CanExecuteChanged -= commandReference.CanExecuteChanged;
            }
            if (newCommand != null)
            {
                newCommand.CanExecuteChanged += commandReference.CanExecuteChanged;
            }
        }

        #endregion

        #region Freezable

        protected override Freezable CreateInstanceCore()
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

DelegateCommand.cs

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;

namespace NestedCommands.Commands
{
    /// <summary>
    ///     This class allows delegating the commanding logic to methods passed as parameters,
    ///     and enables a View to bind commands to objects that are not part of the element tree.
    /// </summary>
    public class DelegateCommand : ICommand
    {
        #region Constructors

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action executeMethod)
            : this(executeMethod, null, false)
        {
        }

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod)
            : this(executeMethod, canExecuteMethod, false)
        {
        }

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod, bool isAutomaticRequeryDisabled)
        {
            if (executeMethod == null)
            {
                throw new ArgumentNullException("executeMethod");
            }

            _executeMethod = executeMethod;
            _canExecuteMethod = canExecuteMethod;
            _isAutomaticRequeryDisabled = isAutomaticRequeryDisabled;
        }

        #endregion

        #region Public Methods

        /// <summary>
        ///     Method to determine if the command can be executed
        /// </summary>
        public bool CanExecute()
        {
            if (_canExecuteMethod != null)
            {
                return _canExecuteMethod();
            }
            return true;
        }

        /// <summary>
        ///     Execution of the command
        /// </summary>
        public void Execute()
        {
            if (_executeMethod != null)
            {
                _executeMethod();
            }
        }

        /// <summary>
        ///     Property to enable or disable CommandManager's automatic requery on this command
        /// </summary>
        public bool IsAutomaticRequeryDisabled
        {
            get
            {
                return _isAutomaticRequeryDisabled;
            }
            set
            {
                if (_isAutomaticRequeryDisabled != value)
                {
                    if (value)
                    {
                        CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers);
                    }
                    else
                    {
                        CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers);
                    }
                    _isAutomaticRequeryDisabled = value;
                }
            }
        }

        /// <summary>
        ///     Raises the CanExecuteChaged event
        /// </summary>
        public void RaiseCanExecuteChanged()
        {
            OnCanExecuteChanged();
        }

        /// <summary>
        ///     Protected virtual method to raise CanExecuteChanged event
        /// </summary>
        protected virtual void OnCanExecuteChanged()
        {
            CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers);
        }

        #endregion

        #region ICommand Members

        /// <summary>
        ///     ICommand.CanExecuteChanged implementation
        /// </summary>
        public event EventHandler CanExecuteChanged
        {
            add
            {
                if (!_isAutomaticRequeryDisabled)
                {
                    CommandManager.RequerySuggested += value;
                }
                CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, 2);
            }
            remove
            {
                if (!_isAutomaticRequeryDisabled)
                {
                    CommandManager.RequerySuggested -= value;
                }
                CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value);
            }
        }

        bool ICommand.CanExecute(object parameter)
        {
            return CanExecute();
        }

        void ICommand.Execute(object parameter)
        {
            Execute();
        }

        #endregion

        #region Data

        private readonly Action _executeMethod = null;
        private readonly Func<bool> _canExecuteMethod = null;
        private bool _isAutomaticRequeryDisabled = false;
        private List<WeakReference> _canExecuteChangedHandlers;

        #endregion
    }

    /// <summary>
    ///     This class allows delegating the commanding logic to methods passed as parameters,
    ///     and enables a View to bind commands to objects that are not part of the element tree.
    /// </summary>
    /// <typeparam name="T">Type of the parameter passed to the delegates</typeparam>
    public class DelegateCommand<T> : ICommand
    {
        #region Constructors

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action<T> executeMethod)
            : this(executeMethod, null, false)
        {
        }

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod)
            : this(executeMethod, canExecuteMethod, false)
        {
        }

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod, bool isAutomaticRequeryDisabled)
        {
            if (executeMethod == null)
            {
                throw new ArgumentNullException("executeMethod");
            }

            _executeMethod = executeMethod;
            _canExecuteMethod = canExecuteMethod;
            _isAutomaticRequeryDisabled = isAutomaticRequeryDisabled;
        }

        #endregion

        #region Public Methods

        /// <summary>
        ///     Method to determine if the command can be executed
        /// </summary>
        public bool CanExecute(T parameter)
        {
            if (_canExecuteMethod != null)
            {
                return _canExecuteMethod(parameter);
            }
            return true;
        }

        /// <summary>
        ///     Execution of the command
        /// </summary>
        public void Execute(T parameter)
        {
            if (_executeMethod != null)
            {
                _executeMethod(parameter);
            }
        }

        /// <summary>
        ///     Raises the CanExecuteChaged event
        /// </summary>
        public void RaiseCanExecuteChanged()
        {
            OnCanExecuteChanged();
        }

        /// <summary>
        ///     Protected virtual method to raise CanExecuteChanged event
        /// </summary>
        protected virtual void OnCanExecuteChanged()
        {
            CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers);
        }

        /// <summary>
        ///     Property to enable or disable CommandManager's automatic requery on this command
        /// </summary>
        public bool IsAutomaticRequeryDisabled
        {
            get
            {
                return _isAutomaticRequeryDisabled;
            }
            set
            {
                if (_isAutomaticRequeryDisabled != value)
                {
                    if (value)
                    {
                        CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers);
                    }
                    else
                    {
                        CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers);
                    }
                    _isAutomaticRequeryDisabled = value;
                }
            }
        }

        #endregion

        #region ICommand Members

        /// <summary>
        ///     ICommand.CanExecuteChanged implementation
        /// </summary>
        public event EventHandler CanExecuteChanged
        {
            add
            {
                if (!_isAutomaticRequeryDisabled)
                {
                    CommandManager.RequerySuggested += value;
                }
                CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, 2);
            }
            remove
            {
                if (!_isAutomaticRequeryDisabled)
                {
                    CommandManager.RequerySuggested -= value;
                }
                CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value);
            }
        }

        bool ICommand.CanExecute(object parameter)
        {
            // if T is of value type and the parameter is not
            // set yet, then return false if CanExecute delegate
            // exists, else return true
            if (parameter == null &&
                typeof(T).IsValueType)
            {
                return (_canExecuteMethod == null);
            }
            return CanExecute((T)parameter);
        }

        void ICommand.Execute(object parameter)
        {
            Execute((T)parameter);
        }

        #endregion

        #region Data

        private readonly Action<T> _executeMethod = null;
        private readonly Func<T, bool> _canExecuteMethod = null;
        private bool _isAutomaticRequeryDisabled = false;
        private List<WeakReference> _canExecuteChangedHandlers;

        #endregion
    }

    /// <summary>
    ///     This class contains methods for the CommandManager that help avoid memory leaks by
    ///     using weak references.
    /// </summary>
    internal class CommandManagerHelper
    {
        internal static void CallWeakReferenceHandlers(List<WeakReference> handlers)
        {
            if (handlers != null)
            {
                // Take a snapshot of the handlers before we call out to them since the handlers
                // could cause the array to me modified while we are reading it.

                EventHandler[] callees = new EventHandler[handlers.Count];
                int count = 0;

                for (int i = handlers.Count - 1; i >= 0; i--)
                {
                    WeakReference reference = handlers[i];
                    EventHandler handler = reference.Target as EventHandler;
                    if (handler == null)
                    {
                        // Clean up old handlers that have been collected
                        handlers.RemoveAt(i);
                    }
                    else
                    {
                        callees[count] = handler;
                        count++;
                    }
                }

                // Call the handlers that we snapshotted
                for (int i = 0; i < count; i++)
                {
                    EventHandler handler = callees[i];
                    handler(null, EventArgs.Empty);
                }
            }
        }

        internal static void AddHandlersToRequerySuggested(List<WeakReference> handlers)
        {
            if (handlers != null)
            {
                foreach (WeakReference handlerRef in handlers)
                {
                    EventHandler handler = handlerRef.Target as EventHandler;
                    if (handler != null)
                    {
                        CommandManager.RequerySuggested += handler;
                    }
                }
            }
        }

        internal static void RemoveHandlersFromRequerySuggested(List<WeakReference> handlers)
        {
            if (handlers != null)
            {
                foreach (WeakReference handlerRef in handlers)
                {
                    EventHandler handler = handlerRef.Target as EventHandler;
                    if (handler != null)
                    {
                        CommandManager.RequerySuggested -= handler;
                    }
                }
            }
        }

        internal static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler)
        {
            AddWeakReferenceHandler(ref handlers, handler, -1);
        }

        internal static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler, int defaultListSize)
        {
            if (handlers == null)
            {
                handlers = (defaultListSize > 0 ? new List<WeakReference>(defaultListSize) : new List<WeakReference>());
            }

            handlers.Add(new WeakReference(handler));
        }

        internal static void RemoveWeakReferenceHandler(List<WeakReference> handlers, EventHandler handler)
        {
            if (handlers != null)
            {
                for (int i = handlers.Count - 1; i >= 0; i--)
                {
                    WeakReference reference = handlers[i];
                    EventHandler existingHandler = reference.Target as EventHandler;
                    if ((existingHandler == null) || (existingHandler == handler))
                    {
                        // Clean up old handlers that have been collected
                        // in addition to the handler that is to be removed.
                        handlers.RemoveAt(i);
                    }
                }
            }
        }
    }
}

View Models

ViewModelBase.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace NestedCommands.ViewModels
{
    class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(object sender, string propertyName)
        {
            if (this.PropertyChanged != null)
            {
                PropertyChanged(sender, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

MainViewModel.cs

using System;
using System.Windows.Input;

namespace NestedCommands.ViewModels
{
    class MainViewModel : ViewModelBase
    {
        public MainViewModel()
        {
            _SummaryViewModel = new SummaryViewModel();
        }

        private SummaryViewModel _SummaryViewModel;
        public SummaryViewModel SummaryViewModel
        {
            get { return _SummaryViewModel; }
            set
            {
                _SummaryViewModel = value;
                OnPropertyChanged(this, "SummaryViewModel");
            }
        }

        public ICommand ShoutCommand
        {
            get { return _SummaryViewModel.ShoutCommand; }
        }
    }
}

SummaryViewModel.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NestedCommands.Commands;
using System.Windows.Input;

namespace NestedCommands.ViewModels
{
    class SummaryViewModel : ViewModelBase
    {
        #region Constructor

        public SummaryViewModel()
        {
            List<SummaryFilterViewModel> filters = new List<SummaryFilterViewModel>();
            filters.Add(new SummaryFilterViewModel("Filter 1"));
            filters.Add(new SummaryFilterViewModel("Filter 2"));
            filters.Add(new SummaryFilterViewModel("Filter 3"));
            Filters = filters;
        }

        #endregion

        #region Properties

        private List<SummaryFilterViewModel> _Filters;
        public List<SummaryFilterViewModel> Filters
        {
            get { return _Filters; }
            set
            {
                _Filters = value;
                OnPropertyChanged(this, "Filters");
            }
        }

        private int _SelectedTabIndex;
        public int SelectedTabIndex
        {
            get { return _SelectedTabIndex; }
            set
            {
                _SelectedTabIndex = value;
                OnPropertyChanged(this, "SelectedTabIndex");
            }
        }

        #endregion

        #region Command References

        public ICommand ShoutCommand
        {
            get { return Filters[SelectedTabIndex].ShoutCommand; }
        }

        #endregion
    }
}

SummaryFilterViewModel.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NestedCommands.Commands;
using System.Windows.Input;

namespace NestedCommands.ViewModels
{
    class SummaryFilterViewModel : ViewModelBase
    {
        #region Constructor

        public SummaryFilterViewModel(string FilterName)
        {
            this.FilterName = FilterName;

            List<string> listData = new List<string>();
            for (int i = 1; i < 10; i++)
            {
                listData.Add(string.Format("{0}: {1}", FilterName, i));
            }
            ListData = listData;
        }

        #endregion

        #region Properties

        private string _FilterName;
        public string FilterName
        {
            get { return _FilterName; }
            set
            {
                _FilterName = value;
                OnPropertyChanged(this, "FilterName");
            }
        }

        private List<string> _ListData;
        public List<string> ListData
        {
            get { return _ListData; }
            set
            {
                _ListData = value;
                OnPropertyChanged(this, "ListData");
            }
        }

        #endregion

        #region Shout Command

        private DelegateCommand _ShoutCommand;
        public ICommand ShoutCommand
        {
            get { return _ShoutCommand ?? (_ShoutCommand = new DelegateCommand(Shout, CanShout)); }
        }

        private void Shout()
        {
            System.Windows.MessageBox.Show(string.Format("Called from SummaryFilterViewModel: {0}", FilterName));
        }

        private bool CanShout()
        {
            return true;
        }

        #endregion
    }
}

回答1:

I think the path you are going down is quickly going to end up complex and tightly coupled. You should probably take a look at using the Mediator Pattern to facilitate communication of changes in your SummaryFilterViewModel to your MainViewModel.

Using the mediator pattern, you can implement a means of subscribing to and publishing messages that allows one view model to communicate with another view model without ending up with tightly coupled view models.

Basically, when your tab selection changes, the summary view model would publish the change with the message payload containing the reference object or other data. The main view model would be subscribed to publication of this message and modify its state accordingly.

Some resources on the Mediator Pattern you can take a look at:

  • http://www.eggheadcafe.com/tutorials/aspnet/ec832ac7-6e4c-4ea8-81ab-7374d3da3425/wpf-and-the-model-view-vi.aspx
  • http://marlongrech.wordpress.com/2008/03/20/more-than-just-mvc-for-wpf/
  • http://marlongrech.wordpress.com/2009/04/16/mediator-v2-for-mvvm-wpf-and-silverlight-applications/


回答2:

I've made some changes to my sample solution in response to some suggestions that were made by Kent Boogaart. Kent, thank you again for your reply it gave me a new direction to move.

I'll try to keep this as short as possible.

  • The MainView is basically a frameset that houses the application's main command interface. In the sample the SummaryView is embedded directly in MainView's XAML. In the real solution it's a content control that may contain different types of child views. Each type of child view may or may not implement the command.
  • I was able to wire the SelectedIndex to a property so that I wouldn't need a dependency on the System.Windows.Control library. When that property changes, I also call OnPropertyChanged for the ShoutCommand property.
  • This, however, did not relay that change to the MainView object. So, in MainViewModel, I listen for the _SummaryViewModel.PropertyChanged event.
  • When MainView hears that the _SummaryViewModel.PropertyChanged event fired, I call OnPropertyChanged(this, "ShoutCommand") which propagates the change to the MainView.

So, I guess I want to know if it's necessary for the MainViewModel to listen to the _SummaryViewModel's PropertyChanged event like I'm doing, or if there is a cleaner way to do it.

My code is listed below: (I tried to take out as much as I could)

Thanks!

MainView

<v:SummaryView Grid.Row="0"
               DataContext="{Binding SummaryViewModel}" />
<Button Content="Shout Command"
        Grid.Row="1"
        Command="{Binding ShoutCommand}" />

MainViewModel

public MainViewModel()
{
    _SummaryViewModel = new SummaryViewModel();
    _SummaryViewModel.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(_SummaryViewModel_PropertyChanged);
}

void _SummaryViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    switch (e.PropertyName)
    {
        case "ShoutCommand":
            OnPropertyChanged(this, "ShoutCommand");
            break;
    }
}

private SummaryViewModel _SummaryViewModel;
public SummaryViewModel SummaryViewModel {...}

public ICommand ShoutCommand
{
    get { return _SummaryViewModel.ShoutCommand; }
}

SummaryView

<TabControl SelectedIndex="{Binding SelectedTabIndex}">
    <TabItem DataContext="{Binding Filters[0]}" Header="{Binding FilterName}">
        <ListBox ItemsSource="{Binding ListData}" />
    </TabItem>
    <!-- TabItem repeated two more times -->
</TabControl>

SummaryViewModel

private List<SummaryFilterViewModel> _Filters;
public List<SummaryFilterViewModel> Filters {...}

private int _SelectedTabIndex;
public int SelectedTabIndex 
{
    get { return _SelectedTabIndex; }
    set
    {
        _SelectedTabIndex = value;
        OnPropertyChanged(this, "SelectedTabIndex");
        OnPropertyChanged(this, "ShoutCommand");
    }
}

public ICommand ShoutCommand
{
    get {
        int selectedTabIndex = SelectedTabIndex;

        return (selectedTabIndex == -1) ? null : Filters[SelectedTabIndex].ShoutCommand; 
    }
}


回答3:

Your post was long and I confess I didn't fully read it. However, I don't understand the purpose of CommandReference. Why not just bind directly to MainViewModel.ShoutCommand? Consider:

  • Bind the ItemsSource of the TabControl to the collection of child view models
  • Bind the SelectedItem of the TabControl to another property that tracks the selected child view model
  • When the aforementioned property changes, raise the PropertyChanged event for the ShoutCommand property, too
  • In the getter for ShoutCommand property, simply return the ShoutCommand of the selected child view model