.NET GUI

.NET Community rund um alle Graphical User Interface (GUI) Themen.
Willkommen bei .NET GUI. Anmeldung | Registrieren | Hilfe | Impressum | Forumsregeln
in Suchen

MVVM Pattern mit WPF verwenden

Letzter Beitrag 08-13-2008 18:07 von Norbert Eder. 9 Antworten.
Seite 1 von 1 (10 Treffer)
Beiträge sortieren: Zurück Weiter
  • 08-06-2008 20:14

    • Norbert Eder
    • Top 10 Mitwirkender
      Männlich
    • Registriert am 04-09-2008
    • Graz / Austria
    • Beiträge 502
    • Punkte 7.469
    • ForumsAdministrator

    MVVM Pattern mit WPF verwenden

    Im früheren Artikel MVC Pattern mit WPF verwenden wurde bereits gezeigt, wie das MVC-Pattern zusammen mit WPF verwendet werden kann. Dieser Artikel möchte sich dem MVVM Pattern widmen und anhand eines Beispiels zeigen, wie dieses Pattern eingesetzt werden kann.

    Das MVC-Pattern besteht aus einem Model, einer View und einem Controller. Das MVVM ist ähnlich aufgebaut, wobei jedoch der Controller durch ein ViewModel ersetzt wird:

    Die View ist nach wie vor der Part, der für die Visualisierung zuständig ist. Das Model liefert die Daten. Das ViewModel besitzt nun Kenntnis über die View und dem Model, kann also Daten entsprechend weiterreichen. Im Normalfall wird das ViewModel per Data Binding an die View gebunden. Daraus entsteht zwar eine enge Kopplung, wobei diese nicht zwangsweise auch an das Model weitergegeben werden muss.

    Auch durch dieses Pattern besteht eine verbesserte Unit-Testbarkeit gegenüber der herkömmlichen Code-Behind-Variante. Ebenfalls kann die Implementierung auf unterschiedliche Personen/Teams verteilt werden.

    Wie dieses Pattern nun konkret implementiert werden kann, soll nachfolgendes Beispiel zeigen.

    Beispielimplementierung

    Eine kleine Beispielanwendung soll Personen in einer Liste darstellen. Zudem soll es ermöglicht werden, diese in einer Detailansicht zu öffnen und zu editieren. Zusätzlich sollen in der Detailansicht der Person zugewiesene Adressen in einer Liste angezeigt werden. Bevor wir zum Sourcecode übergehen, hier (rudimentäre) Screenshots der Anwendung, damit man sich darunter mehr vorstellen kann:

     

    Hierfür werden die notwendigen Basisklassen (Model) benötigt:

    Adressen-Klasse:

    public class Address
    {
        public String Street { get; set; }
        public String StreetNumber { get; set; }
        public String Zip { get; set; }
        public String City { get; set; }
        public String Country { get; set; }
    
        public Address(string street, string streetNumber, string zip, string city)
        {
            Street = street;
            StreetNumber = streetNumber;
            Zip = zip;
            City = city;
        }
    
        public Address(string street, string streetNumber, string zip, string city, string country)
            : this(street, streetNumber, zip, city)
        {
            Country = country;
        }
    }

    Personen-Klasse:

    public class Person
    {
        private List<Address> _addresses = new List<Address>();
    
        public String FirstName { get; set; }
        public String LastName { get; set; }
        public DateTime Birthday { get; set; }
    
        public IList<Address> Addresses
        {
            get { return _addresses; }
        }
    
        public Person(string firstName, string lastName)
        {
            FirstName = firstName;
            LastName = lastName;
        }
    
        public Person(string firstName, string lastName, DateTime birthDay)
            : this(firstName, lastName)
        {
            Birthday = birthDay;
        }
    }

    Für dieses kleine Beispiel ist keine Datenbank-Anbindung vorgesehen. Stattdessen wird ein Mock-Objekt verwendet, welches Beispieldaten generiert und zur Verfügung stellt (also ein Model erstellt):

    public static class Database
    {
        public static Address[ GetAddresses()
        {
            Address[ address = new Address[
            {
                new Address("Eine Strasse", "1", "10000", "Eine Stadt"),
                new Address("Eine andere Straße", "2", "20000", "Eine andere Stadt")
            };
            return address;
        }
    
        public static Person[ GetPeople()
        {
            Person[ people = new Person[
            {
                new Person("Vorname 1", "Nachname 1"),
                new Person("Vorname 2", "Nachname 2")
            };
            return people;
        }
    
        public static Person[ GetCompletePeople()
        {
            Person[ p = GetPeople();
            Address[ a = GetAddresses();
            foreach (Person tempPerson in p)
                foreach (Address tempAddress in a)
                    tempPerson.Addresses.Add(tempAddress);
            return p;
        }
    } 

    Damit wäre alles für unser notwendiges Model erledigt.

    In weiterer Folge wird sowohl für die initiale Anzeige der Personen-Liste, als auch für die Detailansicht die jeweils notwendige View erstellt. Dabei handelt es sich um ein einfaches Steuerelement und um ein Fenster.

    Personen-List-Control

    <UserControl x:Class="DotNetGui.MVVMDemo.PeopleList.PeopleListControl"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:model="clr-namespace:DotNetGui.MVVMDemo.PeopleList.ViewModel"
        Height="Auto" Width="Auto">
    
        <UserControl.Resources>
            <Style TargetType="{x:Type ListViewItem}">
                <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
            </Style>
        </UserControl.Resources>
        <DockPanel LastChildFill="True">
            <ListView DataContext="{Binding Path=.}" ItemsSource="{Binding Path=People}">
                <ListView.View>
                    <GridView>
                        <GridViewColumn Header="FirstName" DisplayMemberBinding="{Binding FirstName, Mode=TwoWay}"/>
                        <GridViewColumn Header="LastName" DisplayMemberBinding="{Binding LastName, Mode=TwoWay}"/>
                        <GridViewColumn Header="Birthday" DisplayMemberBinding="{Binding Birthday, Mode=TwoWay}"/>
                    </GridView>
                </ListView.View>
            </ListView>
        </DockPanel>
    </UserControl>

    Personen-Editor

    <Window x:Class="DotNetGui.MVVMDemo.PersonEditor.PersonEditor"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Edit Person" Height="Auto" Width="Auto" SizeToContent="WidthAndHeight">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
    
            <!-- Labels -->
            <TextBlock Text="First Name" Grid.Column="0" Grid.Row="0"/>
            <TextBlock Text="last Name" Grid.Column="0" Grid.Row="1"/>
            <TextBlock Text="Birthday" Grid.Column="0" Grid.Row="2"/>
            
            <!-- Edit -->
            <TextBox Text="{Binding Path=.FirstName}" Grid.Column="1" Grid.Row="0"/>
            <TextBox Text="{Binding Path=.LastName}" Grid.Column="1" Grid.Row="1"/>
            <TextBox Text="{Binding Path=.Birthday}" Grid.Column="1" Grid.Row="2"/>
            
            <!-- Addresses -->
            <ListView DataContext="{Binding Path=.}" ItemsSource="{Binding Path=Addresses}" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2">
                <ListView.View>
                    <GridView>
                        <GridViewColumn Header="Street" DisplayMemberBinding="{Binding Street, Mode=TwoWay}"/>
                        <GridViewColumn Header="StreetNumber" DisplayMemberBinding="{Binding Path=StreetNumber, Mode=TwoWay}"/>
                        <GridViewColumn Header="Zip" DisplayMemberBinding="{Binding Path=Zip, Mode=TwoWay}"/>
                        <GridViewColumn Header="City" DisplayMemberBinding="{Binding Path=City, Mode=TwoWay}"/>
                        <GridViewColumn Header="Country" DisplayMemberBinding="{Binding Path=Country, Mode=TwoWay}"/>
                    </GridView>
                </ListView.View>
            </ListView>
            <!-- Buttons -->
            <Button Content="Ok" Grid.Column="0" Grid.Row="4" Grid.ColumnSpan="2" Click="Ok_Click"/>
        </Grid>
    </Window>

    Hierbei wird mit einfachen Mitteln (es werden keine Styles etc. verwendet) die Oberfläche designed und alle notwendigen Informationen für das Data Binding definiert.

    Das Personen-List-Control besitzt keinen Sourcecode in der Code Behind Datei. Im Personen-Editor wird lediglich beim Button-Click das Fenster geschlossen. Weiterer Sourcecode ist nicht notwendig.

    Im nächsten Schritt müssen die ViewModels definiert werden, da diese Aufgaben übernehmen und auch für das Data Binding von Relevanz sind. Insgesamt sind 3 ViewModels notwendig:

    • AddressViewModel
    • PersonViewModel
    • PersonListViewModel

    Die ersten beiden ViewModels werden für den Editor verwendet. Das dritte ViewModel dient dem Personen-List-Control für die Anzeige der Personen-Liste. Von Wichtigkeit für dieses Beispiel sind die beiden letzten ViewModels.

    PersonViewModel

    Das PersonViewModel kapselt das Model und implementiert zusätzlich das Interface INotifyPropertyChanged. Damit können Änderungen an den Daten festgestellt und für das Data Binding angezeigt werden. Ebenfalls werden in diesem Fall sämtliche Änderungen im ViewModel sofort an das Model weitergereicht. Dies kann gegebenenfalls entkoppelt gelöst werden.

    public class PersonViewModel : INotifyPropertyChanged
    {
        private Person _person;
        private ObservableCollection<AddressViewModel> _addresses = new ObservableCollection<AddressViewModel>();
    
        public PersonViewModel(Person person)
        {
            _person = person;
    
            foreach (Address tempAddress in _person.Addresses)
                _addresses.Add(new AddressViewModel(tempAddress));
        }
    
        public String FirstName
        {
            get { return _person.FirstName; }
            set 
            {
                if (_person.FirstName != value)
                {
                    _person.FirstName = value;
                    OnPropertyChanged("FirstName");
                }
            }
        }
    
        public string LastName
        {
            get { return _person.LastName; }
            set 
            {
                if (_person.LastName != value)
                {
                    _person.LastName = value;
                    OnPropertyChanged("LastName");
                }
            }
        }
    
        public DateTime Birthday
        {
            get { return _person.Birthday; }
            set 
            {
                if (_person.Birthday != value)
                {
                    _person.Birthday = value;
                    OnPropertyChanged("Birthday");
                }
            }
        }
    
        public ObservableCollection<AddressViewModel> Addresses
        {
            get { return _addresses; }
        }
    
        public bool IsSelected { get; set; }
    
        #region INotifyPropertyChanged Members
    
        protected virtual void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        #endregion
    }

    PersonListViewModel

    Das PersonListViewModel arbeit mit einer ObservableCollection (notwendig für das Data Binding) und hält Objekte vom Type PersonViewModel. Wichtig ist hierbei, dass das ViewModel verwendet wird und nicht das Model selbst, da das ViewModel vom Model enkoppelt sein könnte und mit zusätzlicher Funktionalität ankommt (siehe INotifyPropertyChanged). Weiters stellt das PersonListViewModel einen Command für das Editieren des gewählten Datensatzes bereit. Dieser Command implementiert das Interface ICommand, wodruch ein Standard-WPF-Command implementiert wird. Dieser Command stellt die beiden Methoden CanExecute und Execute zur Verfügung. Erstere wird verwendet, um die Editier-Möglichkeit zu aktivieren bzw. zu deaktiveren - abhängig davon, ob ein Item ausgewählt wurde oder nicht. Innerhalb der Execute-Methode wird das ausgewählte Item in der Detailansicht geöffnet und kann somit bearbeitet werden.

    public class PersonListViewModel
    {
        private ObservableCollection<PersonViewModel> _people = new ObservableCollection<PersonViewModel>();
        private ICommand _editCommand;
    
        public PersonListViewModel(Person[ people)
        {
            foreach (Person tempPerson in people)
                _people.Add(new PersonViewModel(tempPerson));
    
            _editCommand = new EditCommand(this);
        }
    
        public ObservableCollection<PersonViewModel> People
        {
            get { return _people; }
        }
    
        public ICommand Edit
        {
            get { return _editCommand; }
        }
    
        private class EditCommand : ICommand
        {
            PersonListViewModel _listViewModel;
    
            public EditCommand(PersonListViewModel listViewModel)
            {
                _listViewModel = listViewModel;
            }
    
            #region ICommand Members
    
            public bool CanExecute(object parameter)
            {
                foreach (PersonViewModel model in _listViewModel.People)
                    if (model.IsSelected)
                        return true;
                return false;
            }
    
            public event EventHandler CanExecuteChanged;
    
            public void Execute(object parameter)
            {
                PersonViewModel selectedModel = null;
    
                foreach (PersonViewModel model in _listViewModel.People)
                {
                    if (model.IsSelected)
                    {
                        selectedModel = model;
                        break;
                    }
                }
    
                if (selectedModel != null)
                {
                    PersonEditor.PersonEditor editor = new DotNetGui.MVVMDemo.PersonEditor.PersonEditor();
                    editor.DataContext = selectedModel;
                    editor.ShowDialog();
                }
            }
    
            #endregion
        }
    }

    Was nun noch fehlt ist das Hauptfenster für die WPF-Applikation selbst. Dieses ist ebenfalls recht einfach deklariert:

    <Window x:Class="DotNetGui.MVVMDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ui="clr-namespace:DotNetGui.MVVMDemo.PeopleList"
        Title="WPF MVVM Demo" Height="Auto" Width="Auto" SizeToContent="WidthAndHeight">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <ui:PeopleListControl DataContext="{Binding Path=.}" Grid.Row="0"/>
            <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Grid.Row="1">
                <Button Content="Edit" Click="Edit_Click"/>
                <Button Content="Close" Click="Close_Click"/>
            </StackPanel>
        </Grid>
    </Window>

    Zusätzlich befindet sich hier etwas Sourcecode in der Code Behind Datei:

    public partial class MainWindow : Window
    {
        private PersonListViewModel _personListViewModel = null;
    
        public MainWindow()
        {
            InitializeComponent();
    
            Person[ people = Database.GetCompletePeople();
            _personListViewModel = new PersonListViewModel(people);
            this.DataContext = _personListViewModel;
        }
    
        private void Edit_Click(object sender, RoutedEventArgs e)
        {
            _personListViewModel.Edit.Execute(null);
        }
    
        private void Close_Click(object sender, RoutedEventArgs e)
        {
            Application.Current.Shutdown(0);
        }
    }

    Was passiert hier? Es wird lediglich das Model vom Mock-Objekt bezogen, das notwendige PersonListViewModel instanziert und der Datenkontext für das Hauptfenster gesetzt. Auf diesen Kontext greifen die restlichen Views zu und können somit ihre Daten anzeigen. Fertig ist die Beispielanwendung.

    Download Beispielanwendung

     

    Abgelegt unter: , ,
    • Beitragspunkte: 35
    • IP-Adresse ist Registriert
  • 08-06-2008 21:41 Antwort zu

    AW: MVVM Pattern mit WPF verwenden

    Hi Norbert,

    sehr guter Artikel, bin dabei mich durchzuarbeiten. Was mir momentan noch unklar ist, sind die eckigen Klammern in deinem Mock Objekt ( Database ). Da sind einige offene Klammern drin und keine der eckigen Klammern wird geschlossen.
    public static Address[ GetAddresses()
    Wie hier bei Address [ <--
    Mir ist diese Syntax nicht bekannt, vielleicht kannst du mir ein wenig Licht ins Dunkle bringen ;-)

    Grüße
    • Beitragspunkte: 20
    • IP-Adresse ist Registriert
  • 08-07-2008 0:30 Antwort zu

    AW: MVVM Pattern mit WPF verwenden

     Gefällt mir auch gut dieUmsetzung. Ich hätte um den Code einfacher lesbar zu machen für das Beispiel die Datenklassen, sprich also das Model auch so gekennzeichnet. AdressModel, oder einfach zur besseren Lesbarkeit für den Anfänger mit einem [Model]-Attribut versehen. Hab mir den Source nicht angeschaut, vielleicht hast du ja dort entsprechende Kommentare.

    • Beitragspunkte: 20
    • IP-Adresse ist Registriert
  • 08-07-2008 8:22 Antwort zu

    • Norbert Eder
    • Top 10 Mitwirkender
      Männlich
    • Registriert am 04-09-2008
    • Graz / Austria
    • Beiträge 502
    • Punkte 7.469
    • ForumsAdministrator

    AW: MVVM Pattern mit WPF verwenden

    Wäre durchaus eine Möglichkeit gewesen. Aus dem Projektaufbau sollte die Unterteilung jedoch hervorgehen (hoffe ich).
    • Beitragspunkte: 20
    • IP-Adresse ist Registriert
  • 08-07-2008 13:14 Antwort zu

    AW: MVVM Pattern mit WPF verwenden

    Wahrscheinlich werde ich eines der beiden Patterns demnächst verwenden. Welches ist deiner Erfahrung nach besser zur Testbarkeit (UnitTesting) geeignet?

    Die Catainer-Frameworks (Dependency Injection) habe ich mir schon angeschaut (Windsor, Autofac, Unity, NInject, StructureMap und wie sie noch alle heißen) UnitTesting benutze ich sowieso schon seit einiger Zeit. Und zum Mocken habe ich mir jetzt auch mal endlich Frameworks angeschaut. 

     Hast du - gerade in zusammenhang mit dem Patterns MVC, MVP, MVVM - bereits Erfahrungen mit UnitTesting, DependecyInjection und TypeMocking?

    • Beitragspunkte: 20
    • IP-Adresse ist Registriert
  • 08-08-2008 7:51 Antwort zu

    • Norbert Eder
    • Top 10 Mitwirkender
      Männlich
    • Registriert am 04-09-2008
    • Graz / Austria
    • Beiträge 502
    • Punkte 7.469
    • ForumsAdministrator

    AW: MVVM Pattern mit WPF verwenden

    Mariusz Henke:
    Da sind einige offene Klammern drin und keine der eckigen Klammern wird geschlossen.
    public static Address[ GetAddresses()

    Wie hier bei Address [ <--
    Mir ist diese Syntax nicht bekannt, vielleicht kannst du mir ein wenig Licht ins Dunkle bringen ;-)

    Soll einfach ein Array-darstellen. D.h. dort wo die eckige Klammer aufgeht, geht sie auch wieder zu. Keine Ahnung wo das Ding verloren geht, aber dennoch danke für den Hinweis.

    • Beitragspunkte: 20
    • IP-Adresse ist Registriert
  • 08-08-2008 17:27 Antwort zu

    AW: MVVM Pattern mit WPF verwenden

    Ah, wunderbar...dann weiß ich bescheid :-)
    • Beitragspunkte: 5
    • IP-Adresse ist Registriert
  • 08-10-2008 17:45 Antwort zu

    • Norbert Eder
    • Top 10 Mitwirkender
      Männlich
    • Registriert am 04-09-2008
    • Graz / Austria
    • Beiträge 502
    • Punkte 7.469
    • ForumsAdministrator