2016-07-30 4 views
1

Ich versuche, WPF-Ansichtsmodelle zu verstehen.Erweiterte MVVM-Logik - ist das möglich?

Sagen wir, ich habe eine Liste von Elementen. In einigen Fällen möchte ich die Liste auffüllen und den Wert des Elements angeben, das ausgewählt werden soll.

In anderen Fällen möchte ich nur die Liste neu auffüllen und das Steuerelement sollte versuchen, das Element auszuwählen, das zuvor ausgewählt wurde.

In noch anderen Fällen möchte ich die Liste neu auffüllen, nachdem ein Element gelöscht wurde. In diesem Fall ist es nicht möglich, den Artikel auszuwählen, der zuvor ausgewählt wurde, und ich möchte, dass der Code stattdessen den Artikel an der gleichen Listenposition auswählt (oder einen niedrigeren, wenn der letzte Artikel gelöscht wurde).

Normalerweise würde ich nur die Liste füllen, wie ich oben beschrieben habe. Aber wenn ich eine ItemsSource und eine SelectedItem binden muss, scheint das sehr peinlich.

Kann diese Art von Logik in einem MVVM-Design implementiert werden?

+0

Sie können "Auswahl" -Logik in den Setter der Items-Eigenschaft von ViewModel einfügen. Jedes Mal, wenn eine neue Liste gesetzt wird, wird Ihr 'SelectedItem' aktualisiert. – Fabio

Antwort

1

Ich möchte mit Ihnen dieses Beispielprojekt als eine einfache Demonstration der Verwendung von MVVM mit Sammlungen teilen. Ich hoffe, Sie finden es nützlich.

Also, das erste, was Sie tun möchten, ist eine ObservableObject Klasse zu definieren, die Ihnen Zugriff auf INotifyPropertyChanged ermöglicht. Dies ist eine sehr einfache Implementierung, aber es wird in den meisten Situationen gut funktionieren.

using System.ComponentModel; 
namespace WpfApplication8 
{ 
    public class ObservableObject : INotifyPropertyChanged 
    { 
     public event PropertyChangedEventHandler PropertyChanged; 
     public void RaisePropertyChanged(string property) 
     { 
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); 
     } 
    } 
} 

Wir werden dies aus unserer Sicht-Modell implementieren, sowie alle benutzerdefinierten Typen möchten wir INotifyPropertyChanged nutzen.

Diese Demo verwendet eine Klasse Book. Die Book Klasse wird einfach ein paar Felder enthalten, und wir werden auch eine ICommand Implementierung in die gleiche Datei aufnehmen, um das Binden später zu erleichtern.Immer wenn ein ICommand speziell für einen Type gilt, verwende ich gerne die Codedatei dieses Type, um die Dinge sauber gruppiert zu halten.

using System; 
using System.Windows.Input; 

namespace WpfApplication8 
{ 
    public class Book : ObservableObject 
    { 

     private string _title = ""; 
     public string Title 
     { 
      get 
      { 
       return _title; 
      } 
      set 
      { 
       _title = value; 
       RaisePropertyChanged("Title"); 
      } 
     } 

     private string _author = ""; 
     public string Author 
     { 
      get 
      { 
       return _author; 
      } 
      set 
      { 
       _author = value; 
       RaisePropertyChanged("Author"); 
      } 
     } 

    } 
    public abstract class BookCommand : ICommand 
    { 
     public abstract void Execute(object parameter); 
     public virtual bool CanExecute(object parameter) 
     { 
      return parameter is Book; 
     } 
     public event EventHandler CanExecuteChanged 
     { 
      add { CommandManager.RequerySuggested += value; } 
      remove { CommandManager.RequerySuggested -= value; } 
     } 
    } 
} 

Als nächstes werden wir eine ObservableCollection Implementierung benötigen, die uns die volle Kontrolle über das gibt, was geschieht, wenn ein Objekt hinzugefügt oder entfernt wird. Wir wollen auch ICommand Objekte zum Hinzufügen von Büchern entfernen.

Das mag auf den ersten Blick überwältigend erscheinen, aber wenn man es Schritt für Schritt abbricht, ist es nicht so schlimm. Das Ereignis CollectionChanged gibt uns die Möglichkeit, benutzerdefinierte Funktionen hinzuzufügen, wenn ein Element zur Sammlung hinzugefügt oder aus dieser entfernt wird. Dies macht es zum Beispiel recht einfach, ein neues Element aus der Liste auszuwählen, nachdem etwas entfernt wurde. Wir haben auch eine BookCollectionCommand abstrakte Klasse als Grundlage für unsere konkreten ICommand Implementierungen erstellt. Der Grund dafür liegt in der Reduzierung der Redundanz, die je nach Projekt nicht ideal ist, aber hier funktioniert es ganz gut.

Das nächste, was wir brauchen, ist ein Root View-Modell, das direkt an unsere Sicht gebunden ist.

Beachten Sie das Feld NewBookTemplate. Sie haben sich vielleicht gewundert, warum ich den Parameter CMD_AddBookToCollection nicht direkt in die Sammlung geschoben habe. Der Parameter in diesem Muster ist nur eine Vorlage, ein Wegwerfobjekt, das zum Übergeben von Eingaben aus der Ansicht in das Ansichtsmodell verwendet wird. Dies gibt uns zwei sehr wichtige Freiheiten: Wir können das Objekt säubern, bevor es in unsere Sammlung übergeben wird, und wir können vermeiden, ein spezielles Ansichtsmodell nur für das Formular "ein neues Buch hinzufügen" zu schreiben. Die Klasse Book selbst kann als Ansichtsmodell fungieren, was uns sowohl Arbeit spart als auch uns die Freiheit gibt, dieses Objekt in jeder anderen Ansicht wiederzuverwenden. Alles, was wir jetzt brauchen, ist eine grundlegende Ansicht zu implementieren und dies alles zu testen.

<Window x:Class="WpfApplication8.MainWindow" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:local="clr-namespace:WpfApplication8" 
     x:Name="main_window" 
     mc:Ignorable="d" 
     Title="MainWindow" Height="350" Width="525"> 
    <Window.DataContext> 
     <local:ViewModel /> 
    </Window.DataContext> 
    <Grid> 
     <Grid.RowDefinitions> 
      <RowDefinition Height="60" /> 
      <RowDefinition Height="*" /> 
     </Grid.RowDefinitions> 
     <StackPanel Orientation="Horizontal" Grid.Row="0"> 
      <StackPanel.Resources> 
       <Style TargetType="FrameworkElement" x:Key="default_styles"> 
        <Setter Property="Width" Value="100" /> 
        <Setter Property="Height" Value="30" /> 
        <Setter Property="HorizontalAlignment" Value="Center" /> 
        <Setter Property="VerticalAlignment" Value="Center" /> 
        <Setter Property="Margin" Value="5" /> 
       </Style> 
       <Style TargetType="TextBlock" BasedOn="{StaticResource default_styles}" > 
        <Setter Property="Width" Value="50" /> 
       </Style> 
       <Style TargetType="TextBox" BasedOn="{StaticResource default_styles}" /> 
       <Style TargetType="Button" BasedOn="{StaticResource default_styles}" /> 
      </StackPanel.Resources> 
      <TextBlock Text="Title: " /> 
      <TextBox Text="{Binding NewBookTemplate.Title, UpdateSourceTrigger=PropertyChanged}" /> 
      <TextBlock Text="Author: " /> 
      <TextBox Text="{Binding NewBookTemplate.Author, UpdateSourceTrigger=PropertyChanged}" /> 
      <Button Content="Add Book " Command="{Binding MyBooks.AddBook}" CommandParameter="{Binding NewBookTemplate}" /> 
      <Button Content="Remove Book " Command="{Binding MyBooks.RemoveBook}" CommandParameter="{Binding MyBooks.SelectedItem}" /> 
      <TextBlock Text="Count: " /> 
      <TextBlock Text="{Binding MyBooks.Count}" /> 
     </StackPanel> 
     <ListBox Grid.Row="1" ItemsSource="{Binding MyBooks}" SelectedItem="{Binding MyBooks.SelectedItem, UpdateSourceTrigger=PropertyChanged}" > 
      <ListBox.InputBindings> 
       <KeyBinding Key="Delete" Command="{Binding MyBooks.RemoveBook}" CommandParameter="{Binding MyBooks.SelectedItem}" /> 
      </ListBox.InputBindings> 
      <ListBox.ItemTemplate> 
       <DataTemplate> 
        <StackPanel Orientation="Horizontal"> 
         <TextBlock Text="{Binding Title}" /> 
         <TextBlock Text=", " /> 
         <TextBlock Text="{Binding Author}" /> 
        </StackPanel> 
       </DataTemplate> 
      </ListBox.ItemTemplate> 
     </ListBox> 
    </Grid> 
</Window> 

Nun, alles funktioniert auf meinem Ende. Ich hoffe, dieses Beispiel hilft, MVVM etwas leichter zu verdauen. Glückliche Kodierung!

+0

Danke. Ich hatte Gelegenheit, Ihre Antwort genauer zu besprechen. Es gibt eine Menge für mich zu lernen und ein paar Dinge (wie zum Beispiel, warum Sie Klassen für den Befehl "Hinzufügen" und "Entfernen" haben), das macht für mich sehr viel Sinn. Leider habe ich wegen der Einschränkungen von WPF einige Kontrollen, die die Arbeit gewinnen, außer MVVM zu verwenden. Obwohl ich jetzt nicht in der Lage bin, all das Zeug zu lernen, muss ich es besser verstehen, um dieses spezielle Projekt voranzutreiben. –

+0

@ Jonathan Wood WPF ist nicht annähernd so begrenzt wie es zuerst erscheinen mag. Wenn Sie mit WinForms vertraut sind, scheint XAML nicht intuitiv zu sein, aber die Vorteile zeigen sich erst, nachdem die anfängliche Lernkurve vorüber ist. Der Grund für ICommand-Implementierungen besteht darin, die UI-Schicht von der Anwendungslogik zu entkoppeln. Dies ist aus einer Vielzahl von Gründen nützlich. Ich habe keine echte Einschränkung von WPF im Vergleich zu WinForms gefunden. Ich bin mir sicher, dass es einige Nischenaufgaben gibt, für die nur WinForms geeignet sind, aber im Allgemeinen ist WPF eine bemerkenswert flexible Plattform. Es ist nur ein bisschen anders – Jace

2

Kann diese Art von Logik in einem MVVM-Design implementiert werden?

Sicher.
Sie sollten jedoch beachten, dass der Wert SelectedItem in ItemsSource angezeigt werden muss. Deine Gegenstände müssen also eine Identität haben. Zum Beispiel vorstellen Liste der Kunden, von Web-Service geladen:

public class Customer 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
} 

Wenn Art RefreshCommand Implementierung Sie neu wählen Kunden möchten, dass zuvor ausgewählt wurde:

private async void HandleRefreshAsync() 
{ 
    var selectedCustomerId = SelectedCustomer?.Id; 
    Customers = await customersService.GetCustomersAsync(); 
    SelectedCustomer = Customers.FirstOrDefault(_ => _.Id == selectedCustomerId); 
} 

// these are bound to SelectedItem/ItemsSource of Selector control 
public Customer SelectedCustomer { ... } 
public ObservableCollection<Customer> Customers { ... } 

// this is just relay/delegate command, which is handled by HandleRefreshMethod 
public RelayCommand RefreshCommand { ... } 

ähnlicher Ansatz könnte sein, verwendet für andere Fälle, die Sie erwähnten.

+0

Wenn also die selbe Auswahlposition nach dem Löschen eines Gegenstandes beibehalten wird, würde ich nur den Ort des gelöschten Gegenstandes verfolgen und ich könnte den ausgewählten Artikel auf den Artikel an dieser Position in "Kunden" setzen? –

+0

Genau. Der übliche Ansatz besteht darin, einige Werte zu speichern, die nach dem erneuten Laden der Sammlung, dem erneuten Laden der Sammlung und optional dem Wiederherstellen von 'SelectedItem' mit gespeicherten Werten benötigt werden. – Dennis

1

Sie könnten die INotifyCollectionChanged Schnittstelle für Ihre ListBox.ItemsSource in Betracht ziehen. Wenn Sie mit einer spezialisierten Sammlung arbeiten, müssen Sie diese wahrscheinlich selbst implementieren. Sie können jedoch möglicherweise mit der Verwendung eines ObservableCollection<T> (oder Unterklasse) davonkommen, das die oben erwähnte Schnittstelle implementiert. In jedem Fall ermöglicht die Verwendung dieser Schnittstelle, dass sich die ListBox automatisch aktualisiert, wenn die zugrunde liegende Sammlung aktualisiert wird.