2013-03-17 18 views
13

Immaginiamo di avere qualche controllo da parte dell'utente. Il controllo utente ha alcune finestre figlio. E l'utente del controllo utente desidera chiudere le finestre secondarie di qualche tipo. C'è un metodo nel codice di controllo utente dietro:Dare un comando a Visualizza in MVVM

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

Ma io non posso chiamare questo metodo come non ho accesso diretto alla vista.

Un'altra soluzione a cui penso è in qualche modo esporre il controllo utente ViewModel come una delle sue proprietà (quindi posso associarlo e dare il comando direttamente a ViewModel). Ma non voglio che gli utenti del controllo utente sappiano nulla sul controllo utente ViewModel.

Quindi qual è il modo giusto per risolvere questo problema?

risposta

2

Un modo per raggiungere questo obiettivo sarebbe per il modello al fine di richiedere che le finestre figlio devono essere chiuse:

public class ExampleUserControl_ViewModel 
{ 
    public Action ChildWindowsCloseRequested; 

    ... 
} 

la vista sarebbe poi iscriversi alla manifestazione la sua propria visione-modello, e prendersi cura di chiusura le finestre quando viene sparato.

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

     DataContext = viewModel; 
    } 

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

    ... 
} 

Quindi qui il modello di visualizzazione può garantire che le finestre figlio siano chiuse senza avere alcuna conoscenza della vista.

+3

È inoltre possibile impostare DataContext di UserControl sul ViewModel, eliminando la proprietà pubblica ViewModel. Questo richiederà un casting sulla registrazione dell'evento, ma è una buona pratica dato che in MVVM devi comunque impostare UserControl.DataContext su ViewModel. Inoltre, assicurati di eseguire alcune convalide che il tuo ChildWindowsCloseRequested non sia nullo prima di chiamarlo, altrimenti otterrai un'eccezione. –

+0

Vero, aggiornerò la mia risposta, saluti. –

4

ho gestito questo tipo di situazione passata portando nel concetto di un WindowManager, che è un orribile nome per essa, quindi cerchiamo di coppia con un WindowViewModel, che è leggermente meno orribile solo - ma la l'idea di base è:

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

nota: sto solo lanciando tutto questo a casaccio; ovviamente vorresti sintonizzare questa idea per le tue esigenze specifiche.

Tuttavia, la premessa di base è che i comandi possono funzionare sugli oggetti WindowViewModel, attivare il flag IsOpen in modo appropriato e la classe gestore gestisce l'apertura/chiusura di qualsiasi nuova finestra. Ci sono decine di possibili modi per fare questo, ma ha funzionato in un pizzico per me in passato (quando in realtà implementato e non gettato insieme sul mio telefono, che è)

31

mi sento ho appena trovato un piuttosto piacevole MVVM soluzione a questo problema. Ho scritto un comportamento che sta esponendo una proprietà di tipo WindowType e una proprietà booleana Open. DataBinding quest'ultimo consente al ViewModel di aprire e chiudere facilmente le finestre, senza sapere nulla sulla vista.

Devo comportamenti d'amore ...:)

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 (nero/viola simili):

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

Aperto 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, mi piacciono i comportamenti ... – JerKimball

+0

+1 per i comportamenti – chrisw

+0

@adabyron, Perché non fornite la vostra risposta come codice sorgente scaricabile? – RobinAtTech

4

Un modo ragionevole per i puristi è la creazione di un servizio che gestisce la navigazione. Breve sommario: crea un NavigationService, registra la tua vista sul NavigationService e usa NavigationService all'interno del modello di vista per navigare.

Esempio:

class NavigationService 
{ 
    private Window _a; 

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

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

per ottenere un riferimento a NavigationService si potrebbe fare un'astrazione su di esso (cioè INavigationService) e registrare/ottenerlo tramite un CIO. Più appropriatamente si potrebbero anche fare due astrazioni, una che contiene i metodi di registrazione (usati dalla vista) e una che contiene gli attuatori (usati dal modello di vista).

Per un esempio più dettagliato si potrebbe verificare l'attuazione di Gill Cleeren che dipende fortemente IoC:

http://www.silverlightshow.net/video/Applied-MVVM-in-Win8-Webinar.aspx a partire dalle 00:36:30

1

La maggior parte delle risposte a questa domanda comporta una variabile di stato che è controllato dal ViewModel e la vista agisce sulle modifiche a questa variabile. Questo è utile per i comandi di stato come l'apertura o la chiusura di una finestra, o semplicemente per mostrare o nascondere alcuni controlli. Tuttavia, non funziona bene per i comandi di evento stateless . È possibile attivare un'azione sul fronte di salita del segnale, ma è necessario impostare nuovamente il segnale su basso (falso) o non si innescherà mai più.

Ho scritto un articolo sul modello ViewCommand che risolve questo problema. È fondamentalmente la direzione inversa dei normali comandi che vanno dalla vista al ViewModel corrente. Implica un'interfaccia che ogni ViewModel può implementare per inviare comandi a tutte le viste attualmente connesse. Una vista può essere estesa per registrarsi con ciascun ViewModel assegnato quando la sua proprietà DataContext cambia. Questa registrazione aggiunge la vista all'elenco delle viste nel ViewModel. Ogni volta che ViewModel deve eseguire un comando in una vista, passa a tutte le viste registrate e esegue il comando su di esse se esiste. Questo fa uso della riflessione per trovare i metodi ViewCommand nella classe View, ma anche Binding nella direzione opposta.

Il metodo ViewCommand nella classe Vista:

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

questo è chiamato da un ViewModel:

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

L'articolo è disponibile on my website e in una versione precedente di on CodeProject.

Il codice incluso (licenza BSD) fornisce misure per consentire la ridenominazione dei metodi durante l'offuscamento del codice.

Problemi correlati