[英]Give some command to View in MVVM
我們假設我有一些用戶控制權。 用戶控件有一些子窗口。 並且用戶控制用戶想要關閉某種類型的子窗口。 用戶控制代碼中有一種方法:
public void CloseChildWindows(ChildWindowType type)
{
...
}
但我無法調用此方法,因為我無法直接訪問該視圖。
我想到的另一個解決方案是以某種方式將用戶控件ViewModel作為其屬性之一公開(因此我可以綁定它並直接向ViewModel發出命令)。 但我不希望用戶控制用戶知道有關用戶控件ViewModel的任何信息。
那么解決這個問題的正確方法是什么?
我覺得我剛剛找到了一個相當不錯的MVVM解決方案來解決這個問題。 我寫了一個暴露類型屬性WindowType
和布爾屬性Open
。 DataBinding后者允許ViewModel輕松打開和關閉窗口,而不需要了解View。
要愛的行為...... :)
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
}
}
}
}
我在過去通過引入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
標志,並且管理器類處理打開/關閉任何新窗口。 有很多種可能的方法可以做到這一點,但過去它對我來說很有用(實際實現時並沒有在我的手機上一起扔,也就是說)
純粹主義者的合理方式是創建一個處理導航的服務。 簡短摘要:創建NavigationService,在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開始
實現此目的的一種方法是視圖模型請求關閉子窗口:
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
}
...
}
因此,視圖模型可以確保關閉子窗口而不了解視圖。
此問題的大多數答案都涉及由ViewModel控制的狀態變量,而View則對此變量的更改起作用。 這適用於有狀態的命令,如打開或關閉窗口,或只是顯示或隱藏某些控件。 但它對無狀態事件命令不起作用。 您可以在信號的上升沿觸發某些操作,但需要再次將信號設置為低(假),否則它將不再觸發。
我寫了一篇關於ViewCommand模式的文章來解決這個問題。 它基本上是從View到當前ViewModel的常規命令的反方向。 它涉及一個接口,每個ViewModel都可以實現該接口,以向所有當前連接的視圖發送命令。 可以擴展View以在其DataContext屬性更改時向每個已分配的ViewModel注冊。 此注冊將View添加到ViewModel中的Views列表。 每當ViewModel需要在View中運行命令時,它會遍歷所有已注冊的視圖並在它們上運行命令(如果存在)。 這使用反射來查找View類中的ViewCommand方法,但是在相反的方向上綁定也是如此。
View類中的ViewCommand方法:
public partial class TextItemView : UserControl
{
[ViewCommand]
public void FocusText()
{
MyTextBox.Focus();
}
}
這是從ViewModel調用的:
private void OnAddText()
{
ViewCommandManager.Invoke("FocusText");
}
該文章可在我的網站上以及CodeProject上的舊版本中找到 。
包含的代碼(BSD許可證)提供了允許在代碼混淆期間重命名方法的措施。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.