.NET GUI

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

MVC Pattern mit WPF verwenden

Letzter Beitrag 07-11-2008 15:26 von Norbert Eder. 3 Antworten.
Seite 1 von 1 (4 Treffer)
Beiträge sortieren: Zurück Weiter
  • 06-07-2008 20:56

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

    MVC Pattern mit WPF verwenden

    Download Beispielanwendung

    Das MVC (Model View Controller) Pattern hat bereits eine lange Tradition hinter sich. Beschreibt es doch, wie die einzelnen Komponenten einer Anwendung getrennt werden können. Dazu werden durch das Patterns insgesamt drei Teile definiert (für weiterführende Informationen eine der vielen Seiten zu diesem Thema aufsuchen):

    • Model (die anzuzeigenden Daten)
    • View (die visuelle Repräsentation der Daten)
    • Controller (reagiert auf Useraktionen und leitet Aufrufe weiter)

     

    Dieses System bietet einige Vorteile in der Anwendungsentwicklung:

    • Testbarkeit der einzelnen Bestandteile
    • Relativ lose Kopplung zwischen den einzelnen Teilen
    • Gute Erweiterbarkeit

     

    Damit MVC in WPF umgesetzt werden kann, sind einige grundlegende Dinge notwendig, die uns durch WPF jedoch zur Verfügung gestellt werden:

    Im hier beschriebenen Beispiel (Download) werden alle diese Teile eingesetzt und miteinander verbunden, um das MVC Pattern umsetzen zu können. Es muss hinzugefügt werden, dass das angesprochene Pattern für diese Anwendung in der Realität definitiver Overkill wäre, doch sollte das Verfahren damit einfach zu zeigen sein.

    Wie funktioniert es: In einem Hauptfenster wird eine Liste von Personen angezeigt. Wird eine Person ausgewählt, können Detailinformationen angezeigt werden. Zusätzlich besteht die Möglichkeit, die selektierte Person aus der Liste zu entfernen. Grün selektierte Einträge wurden bereits geöffnet und angesehen.

    Für diesen Zweck muss zuerst die notwendige Datenklasse erstellt werden. Diese implementiert das Interface INotifyPropertyChanged. Dies ist notwendig, um bei bestimmten Eigenschaften eine Wertveränderung an das Binding System weitergeben zu können. Anschließend muss eine Collection für Objekte des Typs Person erstellt werden. Dabei handelt es sich um eine ObservableCollection, damit auch hier Veränderungen überwacht werden können.

    Im Anschluss daran können bereits die Views definiert werden. Insgesamt besteht diese kleine Anwendung aus den Teilen MainPersonWindow, dem Steuerelement MainPersonWindowView (wird in MainPersonWindow eingebettet) und dem SelectedPersonViewer (Window). Letzterer implementiert ein Interface namens IPersonViewer. Dieses schreibt die Methode ShowPerson vor. Durch dieses Interface besteht nun die Möglichkeit der losen Koppelung. Es muss für den Aufruf der Methode ShowPerson die tatsächliche View nicht bekannt sein.

    SelectedPersonViewer stellt also die ausgewählte Person dar. Hier nun das XAML dieses Fensters:

    <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="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <!-- Beschriftungen -->
        <TextBlock Text="Firstname:" Grid.Column="0" Grid.Row="0"/>
        <TextBlock Text="Lastname:" Grid.Column="0" Grid.Row="1"/>
        <TextBlock Text="Street:" Grid.Column="0" Grid.Row="2"/>
        <TextBlock Text="Zip:" Grid.Column="0" Grid.Row="3"/>
        <TextBlock Text="City:" Grid.Column="0" Grid.Row="4"/>
        <TextBlock Text="Phone:" Grid.Column="0" Grid.Row="5"/>
        <!-- Anzuzeigende Werte -->
        <!-- Werte werden aus dem gesetzten DataContext bezogen -->
        <TextBlock Text="{Binding FirstName}" Grid.Column="1" Grid.Row="0"/>
        <TextBlock Text="{Binding LastName}" Grid.Column="1" Grid.Row="1"/>
        <TextBlock Text="{Binding City}" Grid.Column="1" Grid.Row="2"/>
        <TextBlock Text="{Binding Zip}" Grid.Column="1" Grid.Row="3"/>
        <TextBlock Text="{Binding City}" Grid.Column="1" Grid.Row="4"/>
        <TextBlock Text="{Binding Phone}" Grid.Column="1" Grid.Row="5"/>
    </Grid>

    Die Codebehind Datei sieht nicht komplizierter aus, es wird lediglich die vom implementierten Interface vorgeschriebene Methode definiert:

    public partial class SelectedPersonViewer : Window, IPersonViewer
    {
        public SelectedPersonViewer()
        {
            InitializeComponent();
        }
    
        #region IPersonViewer Member
    
        /// <summary>
        /// Wird über das Interface aufgerufen und setzt die übergebene Person als DataContext. 
        /// Anschließend wird die View geöffnet. 
        /// </summary>
        /// <param name="person"></param>
        public void ShowPerson(Person person)
        {
            this.DataContext = person;
            this.ShowDialog();
        }
    
        #endregion
    }

    Damit wäre die Anzeige der Personendetails soweit fertig. Nun kommen wir zum Hauptfenster und die darin enthaltene View.

    Das Hauptfenster (MainPersonWindow) zeigt ein Steuerelement an, welches eine Auflistung der Personen enthält. Dieses Steuerelement nennt sich MainPersonWindowView. Das XAML dazu ist ebenfalls recht einfach gestrickt:

    <DockPanel Margin="5" LastChildFill="True">
        <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom">
            <Button Command="{x:Static local:Commands.ShowSelectedPerson}" Content="View Person"/>
            <Button Command="{x:Static local:Commands.DeleteSelectedPerson}" Content="Delete Person"/>
        </StackPanel>
        <!-- Path=. bezieht sich auf den gesetzten DataContext -->
        <ListBox IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Path=.}"/>
    </DockPanel>

    Codebehind besteht zu diesem Steuerlement keines. Hier sind einige Dinge auffällig. Die Listbox enthält ein "Binding Path=.". Dies bedeutet, dass das Binding auf den aktuell gesetzten Datenkontext basiert. Durch das setzen von IsSynchronizedWithCurrentItem auf True ist zudem gewährleistet, dass das angezeigte Element immer dem SelectedItem entspricht.

    Weiters fällt auf, dass die einzelnen Schaltflächen (Buttons) jeweils einen Command zugewiesen haben. Verwiesen wird auf eine statische Command-Klasse, welche die definierten Routed Commands enthält:

    /// <summary>
    /// Statische Klasse für das Auflisten der verfügbaren Commands. Wird für das Command Binding benötigt.
    /// </summary>
    public static class Commands
    {
        public static readonly RoutedUICommand ShowSelectedPerson;
        public static readonly RoutedUICommand DeleteSelectedPerson;
    
        static Commands()
        {
            ShowSelectedPerson = new RoutedUICommand(Resources.ShowSelectedPersonCommandText, "ShowSelectedPerson", typeof(Commands));
            DeleteSelectedPerson = new RoutedUICommand(Resources.DeletedSelectedPersonCommandText, "DeleteSelectedPerson", typeof(Commands));
        }
    }

    Damit wären grundsätzlich die Commands definiert, aber noch wird nichts aufgerufen, noch nichts ausgeführt. Sehen wir uns dazu das XAML für das Hauptfenster genauer an:

    <Window x:Class="MVCSimpleDemo.MainPersonWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MVCSimpleDemo"
        xmlns:views="clr-namespace:MVCSimpleDemo.Views"
        Title="Simple MVC Demo" 
        MinHeight="300" MinWidth="300" 
        MaxHeight="1000" MaxWidth="800"
        SizeToContent="WidthAndHeight"
        WindowStartupLocation="CenterScreen">
        
        <!-- Erstellung der Command Bindings und der notwendigen Eventhandler, welche beschreiben, ob der Command grundsätzlich zur
        Verfügung steht und abgearbeitet werden kann -->
        <Window.CommandBindings>
            <CommandBinding Command="{x:Static local:Commands.ShowSelectedPerson}" CanExecute="ShowSelectedPerson_CanExecute" Executed="ShowSelectedPerson_Executed"/>
            <CommandBinding Command="{x:Static local:Commands.DeleteSelectedPerson}" CanExecute="DeleteSelectedPerson_CanExecute" Executed="DeleteSelectedPerson_Executed"/>
        </Window.CommandBindings>
        
        <views:MainPersonWindowView/>
    </Window>
    

    Darin werden nun so genannte Command Bindings erstellt. Es erfolgt die Angabe des Commands. Ebenfalls werden Eventhandler für CanExecute und Executed gesetzt. Wie im Codebehind zu sehen ist, gehen innerhalb der Eventhandler die Aufrufe an den bekannten Controller, welcher im Hauptfenster definiert wurde:

    public partial class MainPersonWindow : Window
    {
        #region Attributes
    
        private MainPersonWindowController _controller;
    
        #endregion Attributes
    
        #region ctor
    
        public MainPersonWindow()
        {
            InitializeComponent();
    
            PersonCollection persons = PersonCollection.Load();
            _controller = new MainPersonWindowController(this, persons);
    
            this.DataContext = persons;
        }
    
        #endregion ctor
    
        private void ShowSelectedPerson_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = _controller.CanShowSelectedPerson;
        }
    
        private void ShowSelectedPerson_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            _controller.ShowSelectedPerson();
        }
    
        private void DeleteSelectedPerson_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = _controller.CanShowSelectedPerson;
        }
    
        private void DeleteSelectedPerson_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            _controller.DeleteSelectedPerson();
        }
    }

    Tritt das Executed-Event auf, dann wird der entsprechende Eventhandler gestartet und der Aufruf direkt an den Controller weitergegeben. Dieser sorgt in weiterer Folge für die Abarbeitung der Aktion (der Controller wird etwas weiter unten gezeigt). Interessant ist auch CanExecute. Hier kann definiert werden, dass ein Command eventuell nicht ausgeführt werden kann (beispielsweise ist die Liste leer und somit könnte keine Person angezeigt werden). Wird hier ein false gesetzt, dann kann der Command nicht aufgerufen werden. Zudem wird das Element, an dem der Command hängt deaktiviert, kann also ebenfalls nicht verwendet werden. Dies passiert automatisch, ohne dass der Entwickler große Rücksicht darauf nehmen muss.

    Der bereits angesprochene Controller hält eine direkte Referenz zu seiner eigenen View. Dies wird auch durch den Konstruktor des Controllers ersichtlich:

    public MainPersonWindowController(MainPersonWindow personWindow, PersonCollection persons)
    {
        if (personWindow == null)
            throw new ArgumentException("personWindow");
        if (persons == null)
            throw new ArgumentException("persons");
    
        _personWindow = personWindow;
        _personsView = CollectionViewSource.GetDefaultView(persons);
    }

    Hinter _personsView versteckt sich eine ICollectionView, welche die standard View der angegebenen Collection enthält. Damit besteht nun die Möglichkeit, auf selektierte Werte zuzugreifen, unabhängig, ob sich diese in einer ListBox, ListView etc. befinden, so zu sehen in der Methode ShowSelectedPerson:

    public void ShowSelectedPerson()
    {
        Person selectedPerson = _personsView.CurrentItem as Person;
    
        if (selectedPerson != null)
        {
            IPersonViewer personViewer = _personWindow.FindResource("VIEW_SelectedPersonViewer") as IPersonViewer;
            if (personViewer != null)
            {
                personViewer.ShowPerson(selectedPerson);
    
                if (!selectedPerson.WasShown)
                    selectedPerson.WasShown = true;
            }
        }
    }

    Die eigentliche View ist hier nicht bekannt. Das MainPersonWindow besiitzt den Controller, die Datenanzeige findet jedoch in einem andern Steuerelement statt. Damit es darauf keine Referenz geben muss, wurde zuvor bereits ein Interface entworfen. Mit dessen Hilfe kann auf dieses Element zugegriffen werden. Geladen wird es über FindResource, was jedoch bedeutet, dass die View in der Datei App.xaml definiert werden muss:

    <Application.Resources>
        <!-- Zuweisung eines Keys an die entsprechende View, um via FindResource darauf zugreifen zu können -->
        <views:SelectedPersonViewer x:Key="VIEW_SelectedPersonViewer" x:Shared="false"/>
    </Application.Resources>

    Über das Interface kann nun der tatsächlichen View die selektierte Person weitergereicht und zur Anzeige gebracht werden.

    Für weitere Informationen einfach einen Blick in die Beispielanwendung werfen (Visual Studio 2008 Solution). Darin finden sich zusätzliche Kommentare.

     

    Abgelegt unter: , ,
    • Beitragspunkte: 20
    • IP-Adresse ist Registriert
  • 06-20-2008 15:50 Antwort zu

    • jpkleinau
    • Top 10 Mitwirkender
      Männlich
    • Registriert am 05-07-2008
    • Frankfurt am Main
    • Beiträge 35
    • Punkte 505

    AW: MVC Pattern mit WPF verwenden

    Guter Artikel Norbert, habe mich gerade mit Unit Testing und WPF auseinandergesetzt, auch hier kommt man an dem Pattern nicht wirklich vorbei. Josh Smith hat (wie so oft) auch einen guten Artikel dazu geschrieben.

    Alles Gute,

    Jens Peter

    • Beitragspunkte: 20
    • IP-Adresse ist Registriert
  • 07-11-2008 11:50 Antwort zu

    • Dennis
    • Top 25 Mitwirkender
      Männlich
    • Registriert am 04-11-2008
    • G&amp;#246;ppingen
    • Beiträge 18
    • Punkte 440

    AW: MVC Pattern mit WPF verwenden

    Danke für den Artikel.

    Ich selbst tue mich sehr schwer mit der Frage "was verwenden" quälen. Irgendwie gibt es ja 1000 Lösungswege und alle sind mehr schlecht als recht beschrieben. Ich für meinen Teil bin von der Fülle an "Konzepten" einfach schon erschlagen und auch teilweise verängstlich veraltete Konzepte neu zu lernen, die mich nicht weiter bringen. Darum wäre es ganz gut vielleicht noch zu schreiben wo es richtig ist es zu benutzen und wo man lieber etwas anderes benutzen sollte.

    Im Entwurf zeigt sich das Talent, in der Ausführung die Kunst.
    • Beitragspunkte: 20
    • IP-Adresse ist Registriert
  • 07-11-2008 15:26 Antwort zu

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

    AW: MVC Pattern mit WPF verwenden

    Bitte einfach zu diesem Thema im WPF-Forum einen Thread erstellen, dann können wir das ausdiskutieren.

    • Beitragspunkte: 5
    • IP-Adresse ist Registriert
Seite 1 von 1 (4 Treffer)
Powered by Community Server (Commercial Edition)    Hosting powered by 69° media solutions