Обновление ссылки на команду вложенной ViewModel? - PullRequest
1 голос
/ 16 декабря 2010

Я знаю, что, возможно, мне не хватает чего-то простого и очевидного, но на данный момент это ускользает от меня.

Я пытаюсь использовать шаблон MVVM.

Как обновитьссылка на команду в модели представления, которая связана с дочерней моделью представления?

У меня есть представление (MainView), привязанное к модели представления (MainViewModel).На MainView у меня есть экземпляр другого представления (SummaryView), привязанного к модели представления (SummaryViewModel).SummaryViewModel содержит коллекцию третьей модели представления (SummaryFilterViewModel).

В SummaryView есть TabControl, и каждая вкладка на нем связана с одним из экземпляров SummaryFilterViewModel в коллекции SummaryViewodel.

Вкл.В MainView есть кнопка, которая привязана к команде в MainViewModel.

Я хочу, чтобы логика команд находилась в классе SummaryFilterViewModel.Таким образом, любую отображаемую вкладку необходимо подключить к команде, запускаемой кнопкой на MainView.

Я пытался сделать следующее:

  1. Сохранены отдельные объекты SummaryFilterViewModelв коллекции в SummaryViewModel содержатся фактические реализации ShoutCommand.
  2. Объект CommandReference в XAML MainView связывается со свойством ShoutCommand MainViewModel
  3. Свойство ShoutCommand объекта MainViewModel возвращает ссылкуна свойство ShoutCommand объекта SummaryViewModel, хранящегося в MainViewModel.
  4. Свойство ShoutCommand для SummaryViewModel возвращает ссылку на свойство ShoutCommand, в зависимости от того, какая из выбранных в данный момент SummaryFilterViewModel.

Что происходит, что команда не обновляется, когда пользователь меняет вкладки.

Я далеко от базы, как это реализовать?Нужно ли перенести реализацию команды в класс SummaryViewModel?

Заранее благодарен за любую помощь!

Источник моего решения приведен ниже:

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>


Классы команд

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);
                    }
                }
            }
        }
    }
}

Просмотр моделей

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
    }
}

Ответы [ 3 ]

1 голос
/ 17 декабря 2010

Я думаю, что путь, по которому вы идете, быстро станет сложным и тесно связанным. Вам, вероятно, стоит взглянуть на использование Mediator Pattern , чтобы упростить передачу изменений в вашем SummaryFilterViewModel в MainViewModel.

Используя шаблон-посредник, вы можете реализовать средства подписки и публикации сообщений, позволяющие одной модели представления взаимодействовать с другой моделью представления без использования тесно связанных моделей представления.

Как правило, при изменении выбора вкладки модель сводного представления публикует изменение с полезной нагрузкой сообщения, содержащей ссылочный объект или другие данные. Модель основного вида будет подписана на публикацию этого сообщения и соответствующим образом изменит его состояние.

Некоторые ресурсы по шаблону посредника, на которые вы можете посмотреть:

0 голосов
/ 16 декабря 2010

Я внес некоторые изменения в свой пример решения в ответ на некоторые предложения, сделанные Кентом Бугаартом. Кент, еще раз спасибо за твой ответ, он дал мне новое направление движения.

Я постараюсь сделать это как можно короче.

  • MainView - это в основном набор фреймов, в котором находится основной командный интерфейс приложения. В этом примере SummaryView встроен непосредственно в XAML MainView. В реальном решении это элемент управления контентом, который может содержать различные типы дочерних представлений. Каждый тип дочернего представления может реализовывать или не реализовывать команду.
  • Мне удалось связать SelectedIndex со свойством, чтобы мне не понадобилась зависимость от библиотеки System.Windows.Control. Когда это свойство изменяется, я также вызываю OnPropertyChanged для свойства ShoutCommand.
  • Это, однако, не передало это изменение объекту MainView. Итак, в MainViewModel я слушаю событие _SummaryViewModel.PropertyChanged.
  • Когда MainView слышит, что сработало событие _SummaryViewModel.PropertyChanged, я вызываю OnPropertyChanged (это «ShoutCommand»), который передает изменение в MainView.

Итак, я думаю, я хочу знать, необходимо ли MainViewModel прослушивать событие PropertyChanged _SummaryViewModel, как я это делаю, или есть ли более чистый способ сделать это.

Мой код указан ниже: (Я пытался вынуть столько, сколько мог)

Спасибо!

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; 
    }
}
0 голосов
/ 16 декабря 2010

Ваш пост был длинным, и, признаюсь, я его не полностью прочитал. Однако я не понимаю цели CommandReference. Почему бы просто не связать напрямую с MainViewModel.ShoutCommand? Рассмотрим:

  • Свяжите ItemsSource из TabControl с коллекцией дочерних моделей
  • Привязать SelectedItem TabControl к другому свойству, которое отслеживает выбранную модель дочернего представления
  • Когда вышеупомянутое свойство изменяется, вызовите событие PropertyChanged для свойства ShoutCommand тоже
  • В свойстве getter для ShoutCommand просто верните ShoutCommand выбранной модели дочернего представления
...