2013-03-17 11 views
13

Wyobraźmy sobie, że mam jakąś kontrolę nad użytkownikiem. Kontrola użytkownika ma okna potomne. Użytkownik kontroli użytkownika chce zamknąć okna podrzędne pewnego rodzaju. W kodzie kontroli użytkownika znajduje się metoda:Podaj komendę View w MVVM

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

Ale nie mogę nazwać tej metody, ponieważ nie mam bezpośredniego dostępu do widoku.

Innym rozwiązaniem, o którym myślę, jest jakoś odsłonięcie kontroli użytkownika ViewModel jako jednej z jego właściwości (więc mogę go powiązać i przekazać polecenie bezpośrednio do ViewModel). Ale nie chcę, aby użytkownicy kontroli użytkowników wiedzieli coś na temat kontroli użytkownika ViewModel.

Jaki jest właściwy sposób rozwiązania tego problemu?

Odpowiedz

2

Jednym ze sposobów osiągnięcia tego celu byłoby widok model do żądania okna dziecko powinno być zamknięte:

public class ExampleUserControl_ViewModel 
{ 
    public Action ChildWindowsCloseRequested; 

    ... 
} 

Widok byłby następnie zapisać się do imprezy jej zdaniem model, a dbać o zamknięciu okna po uruchomieniu.

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

     DataContext = viewModel; 
    } 

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

    ... 
} 

W tym miejscu model widoku może zapewnić zamknięcie okien podrzędnych bez znajomości widoku.

+3

Możesz także ustawić Kontekst danych UserControl na swoim ViewModelu, pozbywając się właściwości publicznej ViewModel. Będzie to wymagało pewnego rzutowania na rejestrację zdarzeń, ale jest dobrą praktyką, ponieważ w MVVM musisz mimo wszystko ustawić UserControl.DataContext na ViewModel. Upewnij się również, że przed sprawdzeniem poprawność sprawdzenia, czy parametr ChildWindowsCloseRequested nie jest wartością null, lub otrzymasz wyjątek. –

+0

To prawda, zaktualizuję moją odpowiedź, okrzyki. –

4

mam obchodzić tego rodzaju sytuacji w przeszłości poprzez wprowadzenie pojęcia WindowManager, który jest straszne nazwa dla niego, więc niech to powiązać z WindowViewModel, który jest tylko nieznacznie mniej straszne - ale podstawowym założeniem jest:

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

uwaga: ja tylko rzucanie razem to bardzo chaotycznie; oczywiście chciałbyś dostosować ten pomysł do swoich konkretnych potrzeb.

Ale Anywho, podstawowym założeniem jest twoje polecenia mogą działać na WindowViewModel obiektów włączyć flagę IsOpen odpowiednio, a klasa menedżer uchwyty do otwierania/zamykania żadnych nowych okien. Istnieje wiele różnych sposobów, aby to zrobić, ale w przeszłości działało to dla mnie szczypto (kiedy faktycznie zaimplementowano i nie zostało to zrobione na moim telefonie, to jest)

31

Czuję, że znalazłem raczej ładny Rozwiązanie MVVM dla tego problemu. Napisałem zachowanie, które ujawnia właściwość typu WindowType i właściwość boolowską Open. DataBinding to ostatnie umożliwia ViewModel łatwo otworzyć i zamknąć okna, nie wiedząc nic o widoku.

Muszę kochać zachowania ...:)

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 (black/purple podobne):

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

Otwarte CloseWindowBehavior:

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

Ah, kocham zachowania ... – JerKimball

+0

+1 za zachowania – chrisw

+0

@adabyron, dlaczego nie podasz odpowiedzi jako kod źródłowy do pobrania? – RobinAtTech

4

Rozsądnym sposobem dla purystów jest stworzenie usługi, która obsługuje nawigację. Krótkie podsumowanie: utwórz usługę nawigacji, zarejestruj swój widok w usłudze NavigationService i skorzystaj z usługi NavigationService w obrębie modelu widoku, aby nawigować.

Przykład:

class NavigationService 
{ 
    private Window _a; 

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

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

Aby uzyskać odwołanie do NavigationService można było dokonać poboru na wierzchu (tj INavigationService) i zarejestrować/dostać go przez MKOl. Bardziej poprawnie można nawet zrobić dwie abstrakcje, jedną zawierającą metody rejestracji (używane przez widok) i taką, która zawiera siłowniki (używane przez model widoku).

Bardziej szczegółowy przykład można Sprawdź realizację Gill Cleeren który silnie zależy od IoC:

http://www.silverlightshow.net/video/Applied-MVVM-in-Win8-Webinar.aspx zaczynając 00:36:30

1

Większość odpowiedzi na to pytanie wiąże się zmienną stanu, że jest kontrolowany przez ViewModel, a widok działa na zmiany tej zmiennej. Jest to dobre dla poleceń stanowych, takich jak otwieranie lub zamykanie okna, lub po prostu pokazywanie lub ukrywanie niektórych kontrolek. To nie działa jednak dobrze dla bezpaństwowych komend wydarzenie. Możesz wywołać pewne działanie na zboczu narastającym sygnału, ale musisz ponownie ustawić sygnał na niski (fałszywy) lub nigdy go nie wyzwolić.

Napisałem artykuł o wzorze ViewCommand, który rozwiązuje ten problem. Jest to w zasadzie odwrotny kierunek zwykłych poleceń, które przechodzą z widoku do bieżącego ViewModel. Zawiera interfejs, który każdy ViewModel może zaimplementować, aby wysyłać polecenia do wszystkich aktualnie połączonych widoków. Widok można rozszerzyć, aby zarejestrować się w każdym przypisanym modelu ViewModel, gdy zmieni się jego właściwość DataContext. Ta rejestracja dodaje widok do listy widoków w ViewModel. Ilekroć ViewModel musi uruchomić polecenie w widoku, przechodzi przez wszystkie zarejestrowane widoki i uruchamia je na nich, jeśli istnieje. To wykorzystuje odbicie, aby znaleźć metody ViewCommand w klasie View, ale także Binding w przeciwnym kierunku.

Metoda ViewCommand w widoku klasy:

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

Nazywa się to z ViewModel:

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

Artykuł jest dostępny on my website aw starszej wersji on CodeProject.

Dołączony kod (licencja BSD) zapewnia środki umożliwiające zmianę metod podczas zaciemniania kodu.