2009-02-02 16 views
8

Sto cercando di implementare un'applicazione WPF utilizzando il pattern MVVM (Model-View-ViewModel) e vorrei avere la parte View in un assembly separato (un EXE) dalle parti Model e ViewModel (una DLL).Implementazione di MVVM in WPF senza utilizzare System.Windows.Input.ICommand

La svolta consiste nel mantenere l'assembly Model/ViewModel libero da qualsiasi dipendenza WPF. La ragione di questo è che mi piacerebbe riutilizzarlo dagli eseguibili con differenti tecnologie UI (non WPF), ad esempio WinForms o GTK # sotto Mono.

Per impostazione predefinita, questo non può essere fatto, perché ViewModel espone uno o più ICommands. Ma il tipo ICommand è definito nello spazio dei nomi System.Windows.Input, che appartiene al WPF!

Quindi, esiste un modo per soddisfare il meccanismo di binding WPF senza utilizzare ICommand?

Grazie!

+2

@aoven: sono dove eravate quando avete chiesto questo 8 mesi fa, e chiedendo cosa ferisci fare e quanto bene è preoccupato d fuori per voi. Cheers – Berryl

risposta

7

Dovresti essere in grado di definire un singolo comando indirizzato personalizzato WPF nel tuo livello wpf e una singola classe di gestore comandi. Tutte le tue classi WPF possono associare a questo unico comando parametri appropriati.

La classe del gestore può quindi convertire il comando nella propria interfaccia comandi personalizzata definita dall'utente nel livello ViewModel ed indipendente da WPF.

L'esempio più semplice sarebbe un wrapper per un delegato void con un metodo Execute.

Tutti i diversi livelli della GUI devono semplicemente convertire i tipi di comando nativi in ​​tipi di comando personalizzati in un'unica posizione.

+1

puoi fare un esempio? – Jose

+1

per favore vedi sotto per esempio –

4

WinForms non ha l'associazione di dati rich e l'infrastruttura di comandi necessari per utilizzare un modello di visualizzazione di stile MVVM.

Proprio come non è possibile riutilizzare un'applicazione MVC in un'applicazione client (almeno non senza creare montagne di wrapper e adattatori che alla fine rendono più difficile scrivere e eseguire il debug del codice senza fornire alcun valore al cliente) non è possibile riutilizzare un MVVM WPF in un'applicazione WinForms.

Non ho usato GTK # su un progetto reale, quindi non ho idea di cosa possa o non possa fare, ma ho il sospetto che MVVM non sia comunque l'approccio ottimale per GTK #.

Cercare di spostare gran parte del comportamento dell'applicazione nel modello, disporre di un modello di visualizzazione che espone solo i dati dal modello e chiama nel modello in base a comandi senza logica nel modello di vista.

Quindi per WinForms è sufficiente rimuovere il modello di visualizzazione e chiamare direttamente il modello dall'interfaccia utente oppure creare un altro livello intermedio basato su WinForms più limitato supporto di associazione dati.

Ripetere per GTK # o scrivere controller MVC e viste per fornire al modello un front-end Web.

Non cercare di forzare una tecnologia in un modello di utilizzo che è ottimizzato per un altro, non scrivere da zero l'infrastruttura dei comandi (l'ho già fatto prima, non la mia scelta più produttiva), utilizzare il migliore strumenti per ogni tecnologia.

2

Invece dei comandi di esposizione della macchina virtuale, basta esporre i metodi. Quindi utilizzare i comportamenti collegati per associare gli eventi ai metodi oppure, se è necessario un comando, utilizzare un ICommand che può delegare a questi metodi e creare il comando tramite comportamenti collegati.

2

Ovviamente questo è possibile. Puoi creare solo un altro livello di astrazione. Aggiungi la tua interfaccia IMyCommand simile o uguale a ICommand e usala.

Dai un'occhiata alla mia attuale soluzione MVVM che risolve la maggior parte dei problemi che hai citato ma è completamente astratta da cose specifiche della piattaforma e può essere riutilizzata. Inoltre non ho usato nessun code-behind vincolante solo con DelegateCommands che implementa ICommand. La finestra di dialogo è fondamentalmente una vista, un controllo separato che ha il proprio ViewModel e viene mostrato dal ViewModel della schermata principale, ma attivato dall'interfaccia utente tramite DelagateCommand binding.

Visualizza intera soluzione Silverlight 4 qui Modal dialogs with MVVM and Silverlight 4

+0

Answer dimostrando questo approccio [qui] (http: // stackoverflow.it/a/19795332/954927) – Neutrino

4

avevo bisogno di un esempio di questo così ho scritto uno con varie tecniche.

Ho avuto un paio di obiettivi di progettazione in mente

1 - mantenere le cose semplici

2 - assolutamente no code-behind nella visualizzazione (classe Window)

3 - dimostrare una dipendenza di soli il riferimento di sistema nella libreria di classi ViewModel.

4 - mantenere la logica aziendale nel ViewModel e instradare direttamente ai metodi appropriati senza scrivere una serie di metodi "stub".

Ecco il codice ...

App.xaml (senza StartupUri è l'unica cosa da notare)

<Application 
    x:Class="WpfApplicationCleanSeparation.App" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> 
</Application> 

App.xaml.cs (caricare la vista principale)

using System.Windows; 
using WpfApplicationCleanSeparation.ViewModels; 

namespace WpfApplicationCleanSeparation 
{ 
    public partial class App 
    { 
     protected override void OnStartup(StartupEventArgs e) 
     { 
      var view = new MainView(); 
      var viewModel = new MainViewModel(); 

      view.InitializeComponent(); 
      view.DataContext = viewModel; 
      CommandRouter.WireMainView(view, viewModel); 
      view.Show(); 
     } 
    } 
} 

CommandRouter.cs (la magia)

using System.Windows.Input; 
using WpfApplicationCleanSeparation.ViewModels; 

namespace WpfApplicationCleanSeparation 
{ 
    public static class CommandRouter 
    { 
     static CommandRouter() 
     { 
      IncrementCounter = new RoutedCommand(); 
      DecrementCounter = new RoutedCommand(); 
     } 

     public static RoutedCommand IncrementCounter { get; private set; } 
     public static RoutedCommand DecrementCounter { get; private set; } 

     public static void WireMainView(MainView view, MainViewModel viewModel) 
     { 
      if (view == null || viewModel == null) return; 

      view.CommandBindings.Add(
       new CommandBinding(
        IncrementCounter, 
        (λ1, λ2) => viewModel.IncrementCounter(), 
        (λ1, λ2) => 
         { 
          λ2.CanExecute = true; 
          λ2.Handled = true; 
         })); 
      view.CommandBindings.Add(
       new CommandBinding(
        DecrementCounter, 
        (λ1, λ2) => viewModel.DecrementCounter(), 
        (λ1, λ2) => 
         { 
          λ2.CanExecute = true; 
          λ2.Handled = true; 
         })); 
     } 
    } 
} 

MainView.xaml (c'è nessuna code-behind, letteralmente cancellato!)

<Window 
    x:Class="WpfApplicationCleanSeparation.MainView" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:WpfApplicationCleanSeparation="clr-namespace:WpfApplicationCleanSeparation" 
    Title="MainWindow" 
    Height="100" 
    Width="100"> 
    <StackPanel> 
     <TextBlock Text="{Binding Counter}"></TextBlock> 
     <Button Content="Decrement" Command="WpfApplicationCleanSeparation:CommandRouter.DecrementCounter"></Button> 
     <Button Content="Increment" Command="WpfApplicationCleanSeparation:CommandRouter.IncrementCounter"></Button> 
    </StackPanel> 
</Window> 

MainViewModel.cs (include il modello attuale e dal momento che questo esempio è così semplificato, vi prego di scusare il deragliamento del pattern MVVM.

using System.ComponentModel; 

namespace WpfApplicationCleanSeparation.ViewModels 
{ 
    public class CounterModel 
    { 
     public int Data { get; private set; } 

     public void IncrementCounter() 
     { 
      Data++; 
     } 

     public void DecrementCounter() 
     { 
      Data--; 
     } 
    } 

    public class MainViewModel : INotifyPropertyChanged 
    { 
     private CounterModel Model { get; set; } 
     public event PropertyChangedEventHandler PropertyChanged = delegate { }; 

     public MainViewModel() 
     { 
      Model = new CounterModel(); 
     } 

     public int Counter 
     { 
      get { return Model.Data; } 
     } 

     public void IncrementCounter() 
     { 
      Model.IncrementCounter(); 

      PropertyChanged(this, new PropertyChangedEventArgs("Counter")); 
     } 

     public void DecrementCounter() 
     { 
      Model.DecrementCounter(); 

      PropertyChanged(this, new PropertyChangedEventArgs("Counter")); 
     } 
    } 
} 

Proof

Solo un rapido e sporco e spero che sia utile a qualcuno. Ho visto alcuni approcci diversi attraverso vari Google, ma nulla era altrettanto semplice e facile da implementare con la minima quantità di codice possibile che volevo. Se c'è un modo per semplificare ulteriormente, fammi sapere, grazie.

Felice Coding :)

EDIT: Per semplificare il mio codice, si potrebbe trovare questo utile per fare l'Aggiunge in one-liners.

private static void Wire(this UIElement element, RoutedCommand command, Action action) 
    { 
     element.CommandBindings.Add(new CommandBinding(command, (sender, e) => action(), (sender, e) => { e.CanExecute = true; })); 
    } 
+0

'λ1, λ2' è una specie di figo. –

+0

Mi sono imbattuto nella sintassi dei caratteri lambda da qualche parte qui in SO e mi piacerebbe dare il merito: è appena rimasto con me. –

+0

Ho un approccio diverso [qui] (http://stackoverflow.com/a/19795332/954927) – Neutrino

1

Penso che tu stia separando il tuo Progetto nel punto sbagliato. Penso che dovresti condividere solo le tue classi di modelli e di business logic.

VM è un adattamento del modello per soddisfare le viste WPF. Vorrei mantenere la VM semplice e fare proprio questo.

Non riesco a immaginare di forzare MVVM su Winforms. OTOH con la semplice logica di bus &, è possibile immetterli direttamente in un modulo se necessario.

0

"non si può riutilizzare un WPF MVVM in un'applicazione WinForms"

Per questo si prega di vedere URL http://waf.codeplex.com/, ho usato MVVM in Win forma, ora whenver vorrei aggiornare la presentazione dell'applicazione da Win Modulo per WPF, verrà modificato senza modifiche nella logica dell'applicazione,

Tuttavia, ho un problema con il riutilizzo di ViewModel in Asp.net MVC, così posso rendere la stessa applicazione desktop Win in Web senza o con modifiche nella logica dell'applicazione.

Grazie ...

2

Scusa Dave ma non mi è piaciuta molto la tua soluzione. Innanzitutto devi codificare manualmente l'impianto idraulico per ciascun comando in codice, quindi devi configurare il CommandRouter per sapere su ogni associazione vista/vista modello nell'applicazione.

Ho seguito un approccio diverso.

Possiedo un assembly di utilità Mvvm (che non ha dipendenze WPF) e che utilizzo nel mio viewmodel. In quell'assembly dichiaro un'interfaccia ICommand personalizzata e una classe DelegateCommand che implementa quell'interfaccia.

namespace CommonUtil.Mvvm 
{ 
    using System; 


    public interface ICommand 
    { 
     void Execute(object parameter); 
     bool CanExecute(object parameter); 

     event EventHandler CanExecuteChanged; 
    } 

    public class DelegateCommand : ICommand 
    { 
     public DelegateCommand(Action<object> execute) : this(execute, null) 
     { 

     } 

     public DelegateCommand(Action<object> execute, Func<object, bool> canExecute) 
     { 
      _execute = execute; 
      _canExecute = canExecute; 
     } 

     public void Execute(object parameter) 
     { 
      _execute(parameter); 
     } 

     public bool CanExecute(object parameter) 
     { 
      return _canExecute == null || _canExecute(parameter); 
     } 


     public event EventHandler CanExecuteChanged; 

     private readonly Action<object> _execute; 
     private readonly Func<object, bool> _canExecute; 
    } 
} 

Ho anche un gruppo di WPF biblioteca (che fa riferimento alle librerie di sistema WPF), che ho di riferimento dal mio progetto WPF UI. In quell'assembly dichiaro una classe CommandWrapper che ha l'interfaccia standard System.Windows.Input.ICommand. CommandWrapper è costruito utilizzando un'istanza del mio ICommand personalizzato e semplicemente delega Execute, CanExecute e CanExecuteChanged direttamente al mio tipo ICommand personalizzato.

namespace WpfUtil 
{ 
    using System; 
    using System.Windows.Input; 


    public class CommandWrapper : ICommand 
    { 
     // Public. 

     public CommandWrapper(CommonUtil.Mvvm.ICommand source) 
     { 
      _source = source; 
      _source.CanExecuteChanged += OnSource_CanExecuteChanged; 
      CommandManager.RequerySuggested += OnCommandManager_RequerySuggested; 
     } 

     public void Execute(object parameter) 
     { 
      _source.Execute(parameter); 
     } 

     public bool CanExecute(object parameter) 
     { 
      return _source.CanExecute(parameter); 
     } 

     public event System.EventHandler CanExecuteChanged = delegate { }; 


     // Implementation. 

     private void OnSource_CanExecuteChanged(object sender, EventArgs args) 
     { 
      CanExecuteChanged(sender, args); 
     } 

     private void OnCommandManager_RequerySuggested(object sender, EventArgs args) 
     { 
      CanExecuteChanged(sender, args); 
     } 

     private readonly CommonUtil.Mvvm.ICommand _source; 
    } 
} 

Nel mio assemblaggio WPF creo anche una ValueConverter che quando passò un caso di mia abitudine ICommand sputa fuori un'istanza della CommandWrapper compatibile Windows.Input.ICommand.

namespace WpfUtil 
{ 
    using System; 
    using System.Globalization; 
    using System.Windows.Data; 


    public class CommandConverter : IValueConverter 
    { 

     public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 
     { 
      return new CommandWrapper((CommonUtil.Mvvm.ICommand)value); 
     } 

     public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 
     { 
      throw new System.NotImplementedException(); 
     } 
    } 
} 

Ora i miei ViewModels può esporre comandi come le istanze del mio tipo di comando personalizzato senza dover avere alcuna dipendenza da WPF, e la mia interfaccia utente può legarsi Windows.Input.ICommand comandi per quei ViewModels usando il mio ValueConverter in questo modo. (Lo spazio dei nomi XAML è spam).

<Window x:Class="Project1.MainWindow"> 

    <Window.Resources> 
     <wpf:CommandConverter x:Key="_commandConv"/> 
    </Window.Resources> 

    <Grid> 
     <Button Content="Button1" Command="{Binding CustomCommandOnViewModel, 
             Converter={StaticResource _commandConv}}"/> 
    </Grid> 

</Window> 

Ora, se sono davvero pigro (che sono io) e non può essere preso la briga di dover applicare manualmente il CommandConverter ogni volta poi nella mia assemblea WPF posso creare il mio sottoclasse di rilegatura in questo modo:

namespace WpfUtil 
{ 
    using System.Windows.Data; 


    public class CommandBindingExtension : Binding 
    { 
     public CommandBindingExtension(string path) : base(path) 
     { 
      Converter = new CommandConverter(); 
     } 
    } 
} 

così ora posso legare al mio tipo di comando personalizzato ancora più semplicemente in questo modo:

<Window x:Class="Project1.MainWindow" 
       xmlns:wpf="clr-namespace:WpfUtil;assembly=WpfUtil"> 

    <Window.Resources> 
     <wpf:CommandConverter x:Key="_commandConv"/> 
    </Window.Resources> 

    <Grid> 
     <Button Content="Button1" Command="{wpf:CommandBinding CustomCommandOnViewModel}"/> 
    </Grid> 

</Window> 
+0

+1, non essere dispiaciuto, spero che qualcuno più intelligente sia arrivato in 2 anni, questo è quello che SO è per ;). Ho appena scremato la tua risposta, ma mi piace l'astrazione aggiunta e ciò che porta. "Posso creare la mia sottoclasse Binding in questo modo" = terribilmente sexy, mi piace;) –