Подтвердить что ты не робот

Дайте команду View в MVVM

Предположим, у меня есть пользовательский контроль. Пользовательский элемент управления имеет несколько дочерних окон. Пользователь user control хочет закрыть дочерние окна какого-либо типа. В коде управления пользователя есть метод:

public void CloseChildWindows(ChildWindowType type)
{
   ...
}

Но я не могу вызвать этот метод, поскольку у меня нет прямого доступа к представлению.

Другое решение, о котором я думаю, заключается в том, чтобы каким-то образом разоблачить пользовательский элемент управления ViewModel как один из его свойств (поэтому я могу связать его и дать команду непосредственно ViewModel). Но я не хочу, чтобы пользователи пользовательского контроля ничего не знали о пользовательском управлении ViewModel.

Итак, каков правильный способ решить эту проблему?

4b9b3361

Ответ 1

Я чувствую, что я нашел довольно приятное решение MVVM для этой проблемы. Я написал поведение, демонстрирующее свойство type WindowType и логическое свойство Open. DataBinding последнего позволяет ViewModel легко открывать и закрывать окна, ничего не зная о представлении.

Должен любить поведение...:)

enter image description here

Xaml:

<UserControl x:Class="WpfApplication1.OpenCloseWindowDemo"
             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" 
             xmlns:local="clr-namespace:WpfApplication1"
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">

    <UserControl.DataContext>
        <local:ViewModel />
    </UserControl.DataContext>
    <i:Interaction.Behaviors>
        <!-- TwoWay binding is necessary, otherwise after user closed a window directly, it cannot be opened again -->
        <local:OpenCloseWindowBehavior WindowType="local:BlackWindow" Open="{Binding BlackOpen, Mode=TwoWay}" />
        <local:OpenCloseWindowBehavior WindowType="local:YellowWindow" Open="{Binding YellowOpen, Mode=TwoWay}" />
        <local:OpenCloseWindowBehavior WindowType="local:PurpleWindow" Open="{Binding PurpleOpen, Mode=TwoWay}" />
    </i:Interaction.Behaviors>
    <UserControl.Resources>
        <Thickness x:Key="StdMargin">5</Thickness>
        <Style TargetType="Button" >
            <Setter Property="MinWidth" Value="60" />
            <Setter Property="Margin" Value="{StaticResource StdMargin}" />
        </Style>
        <Style TargetType="Border" >
            <Setter Property="Margin" Value="{StaticResource StdMargin}" />
        </Style>
    </UserControl.Resources>

    <Grid>
        <StackPanel>
            <StackPanel Orientation="Horizontal">
                <Border Background="Black" Width="30" />
                <Button Content="Open" Command="{Binding OpenBlackCommand}" CommandParameter="True" />
                <Button Content="Close" Command="{Binding OpenBlackCommand}" CommandParameter="False" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Border Background="Yellow" Width="30" />
                <Button Content="Open" Command="{Binding OpenYellowCommand}" CommandParameter="True" />
                <Button Content="Close" Command="{Binding OpenYellowCommand}" CommandParameter="False" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Border Background="Purple" Width="30" />
                <Button Content="Open" Command="{Binding OpenPurpleCommand}" CommandParameter="True" />
                <Button Content="Close" Command="{Binding OpenPurpleCommand}" CommandParameter="False" />
            </StackPanel>
        </StackPanel>
    </Grid>
</UserControl>

YellowWindow (черный/фиолетовый):

<Window x:Class="WpfApplication1.YellowWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="YellowWindow" Height="300" Width="300">
    <Grid Background="Yellow" />
</Window>

ViewModel, ActionCommand:

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

namespace WpfApplication1
{
    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private bool _blackOpen;
        public bool BlackOpen { get { return _blackOpen; } set { _blackOpen = value; OnPropertyChanged("BlackOpen"); } }

        private bool _yellowOpen;
        public bool YellowOpen { get { return _yellowOpen; } set { _yellowOpen = value; OnPropertyChanged("YellowOpen"); } }

        private bool _purpleOpen;
        public bool PurpleOpen { get { return _purpleOpen; } set { _purpleOpen = value; OnPropertyChanged("PurpleOpen"); } }

        public ICommand OpenBlackCommand { get; private set; }
        public ICommand OpenYellowCommand { get; private set; }
        public ICommand OpenPurpleCommand { get; private set; }


        public ViewModel()
        {
            this.OpenBlackCommand = new ActionCommand<bool>(OpenBlack);
            this.OpenYellowCommand = new ActionCommand<bool>(OpenYellow);
            this.OpenPurpleCommand = new ActionCommand<bool>(OpenPurple);
        }

        private void OpenBlack(bool open) { this.BlackOpen = open; }
        private void OpenYellow(bool open) { this.YellowOpen = open; }
        private void OpenPurple(bool open) { this.PurpleOpen = open; }

    }

    public class ActionCommand<T> : ICommand
    {
        public event EventHandler CanExecuteChanged;
        private Action<T> _action;

        public ActionCommand(Action<T> action)
        {
            _action = action;
        }

        public bool CanExecute(object parameter) { return true; }

        public void Execute(object parameter)
        {
            if (_action != null)
            {
                var castParameter = (T)Convert.ChangeType(parameter, typeof(T));
                _action(castParameter);
            }
        }
    }
}

OpenCloseWindowBehavior:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

namespace WpfApplication1
{
    public class OpenCloseWindowBehavior : Behavior<UserControl>
    {
        private Window _windowInstance;

        public Type WindowType { get { return (Type)GetValue(WindowTypeProperty); } set { SetValue(WindowTypeProperty, value); } }
        public static readonly DependencyProperty WindowTypeProperty = DependencyProperty.Register("WindowType", typeof(Type), typeof(OpenCloseWindowBehavior), new PropertyMetadata(null));

        public bool Open { get { return (bool)GetValue(OpenProperty); } set { SetValue(OpenProperty, value); } }
        public static readonly DependencyProperty OpenProperty = DependencyProperty.Register("Open", typeof(bool), typeof(OpenCloseWindowBehavior), new PropertyMetadata(false, OnOpenChanged));

        /// <summary>
        /// Opens or closes a window of type 'WindowType'.
        /// </summary>
        private static void OnOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var me = (OpenCloseWindowBehavior)d;
            if ((bool)e.NewValue)
            {
                object instance = Activator.CreateInstance(me.WindowType);
                if (instance is Window)
                {
                    Window window = (Window)instance;
                    window.Closing += (s, ev) => 
                    {
                        if (me.Open) // window closed directly by user
                        {
                            me._windowInstance = null; // prevents repeated Close call
                            me.Open = false; // set to false, so next time Open is set to true, OnOpenChanged is triggered again
                        }
                    }; 
                    window.Show();
                    me._windowInstance = window;
                }
                else
                {
                    // could check this already in PropertyChangedCallback of WindowType - but doesn't matter until someone actually tries to open it.
                    throw new ArgumentException(string.Format("Type '{0}' does not derive from System.Windows.Window.", me.WindowType));
                }
            }
            else 
            {
                if (me._windowInstance != null)
                    me._windowInstance.Close(); // closed by viewmodel
            }
        }
    }
}

Ответ 2

Я занимался подобной ситуацией в прошлом, введя понятие a WindowManager, которое является ужасным именем для него, поэтому давайте соединить его с WindowViewModel, который немного менее ужасен, но основная идея:

public class WindowManager
{
    public WindowManager()
    {
        VisibleWindows = new ObservableCollection<WindowViewModel>();
        VisibleWindows.CollectionChanged += OnVisibleWindowsChanged;            
    }
    public ObservableCollection<WindowViewModel> VisibleWindows {get; private set;}
    private void OnVisibleWindowsChanged(object sender, NotifyCollectionChangedEventArgs args)
    {
        // process changes, close any removed windows, open any added windows, etc.
    }
}

public class WindowViewModel : INotifyPropertyChanged
{
    private bool _isOpen;
    private WindowManager _manager;
    public WindowViewModel(WindowManager manager)
    {
        _manager = manager;
    }
    public bool IsOpen 
    { 
        get { return _isOpen; } 
        set 
        {
            if(_isOpen && !value)
            {
                _manager.VisibleWindows.Remove(this);
            }
            if(value && !_isOpen)
            {
                _manager.VisibleWindows.Add(this);
            }
            _isOpen = value;
            OnPropertyChanged("IsOpen");
        }
    }    

    public event PropertyChangedEventHandler PropertyChanged = delegate {};
    private void OnPropertyChanged(string name)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
}

Примечание: я просто собираю это вместе очень беспорядочно; вы, конечно, хотите настроить эту идею на свои конкретные нужды.

Но любой, основная предпосылка - ваши команды могут работать с объектами WindowViewModel, соответствующим образом переключаться на флаг IsOpen, а класс менеджера обрабатывает открытие/закрытие любых новых окон. Есть десятки возможных способов сделать это, но в прошлом это работало для меня (когда на самом деле реализовано и не сбрасывалось вместе на моем телефоне, то есть)

Ответ 3

Разумным способом для пуристов является создание службы, которая обрабатывает вашу навигацию. Краткое описание: создайте навигационную службу, зарегистрируйте свое представление в NavigationService и используйте NavigationService из модели просмотра для навигации.

Пример:

class NavigationService
{
    private Window _a;

    public void RegisterViewA(Window a) { _a = a; }

    public void CloseWindowA() { a.Close(); }
}

Чтобы получить ссылку на NavigationService, вы можете сделать абстракцию поверх нее (т.е. INavigationService) и зарегистрировать/получить ее через IoC. Более корректно вы можете сделать две абстракции: одну, содержащую методы регистрации (используемые в представлении) и одну, содержащую исполнительные механизмы (используемые моделью просмотра).

Для более подробного примера вы можете проверить реализацию Gill Cleeren, которая сильно зависит от IoC:

http://www.silverlightshow.net/video/Applied-MVVM-in-Win8-Webinar.aspx, начинающийся в 00:36:30

Ответ 4

Одним из способов достижения этой цели было бы, чтобы модель вида запросила закрыть дочерние окна:

public class ExampleUserControl_ViewModel
{
    public Action ChildWindowsCloseRequested;

    ...
}

Затем представление будет подписано на его событие модели представления и позаботится о закрытии окон при его запуске.

public class ExampleUserControl : UserControl
{
    public ExampleUserControl()
    {
        var viewModel = new ExampleUserControl_ViewModel();
        viewModel.ChildWindowsCloseRequested += OnChildWindowsCloseRequested;

        DataContext = viewModel;
    }

    private void OnChildWindowsCloseRequested()
    {
        // ... close child windows
    }

    ...
}

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

Ответ 5

В большинстве ответов на этот вопрос включена переменная состояния, которая контролируется ViewModel, а функция View действует на изменения этой переменной. Это полезно для команд с состоянием, таких как открытие или закрытие окна или просто отображение или скрытие некоторых элементов управления. Это не работает для команд событий без состояния. Вы можете вызвать какое-то действие по нарастающему фронту сигнала, но вам нужно снова установить сигнал на низкий (false) или он больше никогда не будет запускаться.

Я написал статью о шаблоне ViewCommand, который решает эту проблему. Это в основном обратное направление регулярных команд, которые идут от представления к текущему ViewModel. Он включает в себя интерфейс, который каждый ViewModel может реализовать для отправки команд всем связанным в настоящее время Views. Просмотр может быть расширен для регистрации в каждой назначенной ViewModel, когда изменяется его свойство DataContext. Эта регистрация добавляет представление в список представлений в ViewModel. Всякий раз, когда ViewModel должен запускать команду в представлении, он просматривает все зарегистрированные виды и запускает на них команду, если она существует. Это использует отражение, чтобы найти методы ViewCommand в классе View, но также делает привязку в противоположном направлении.

Метод ViewCommand в классе View:

public partial class TextItemView : UserControl
{
    [ViewCommand]
    public void FocusText()
    {
        MyTextBox.Focus();
    }
}

Это вызвано из ViewModel:

private void OnAddText()
{
    ViewCommandManager.Invoke("FocusText");
}

Статья доступна на моем веб-сайте и в более старой версии on CodeProject.

Входящий в комплект код (лицензия BSD) предоставляет меры, позволяющие использовать методы переименования во время обфускации кода.