Freigeben über


Model-View-ViewModel

Jag talar ofta med kunder och partners som vill använda vedertagna designmönster för att separera ansvaret som de olika delarna en klientapplikation har. Syftet att göra dem enklare att testa och enklare att underhålla. En annan motivation för att skapa en renare uppdelning av gränssnitt och logik är att designers/interaktionsdesigners kan arbeta med gränssnitt och layout/utseende, medan utvecklare helt separat kan arbeta med beteenden och den underliggande affärslogiken på ett mer effektivt sätt.

Ett designmönster som har lyfts fram av Microsoft på senare tid är MVC-mönstret: där MVC står för Model-View-Controller. MVC är ett mönster som mycket väl lämpar sig för “traditionella” webbapplikationer och som passar bra för webbens tillståndslösa beteende. Klienten (webbläsaren) skickar ett HTTP-anrop som tas emot av en Controller som bestämmer hur det ska hanteras. Controllern kan använda sig av en Model och dess affärslogik för att behandla/hämta data som sedan lämnas över till en vy (View). Vyn ansvarar sedan för att rendera resultatet som skickas tillbaka till klienten.

När det gäller rika klienter som håller ett internt tillstånd eller “state” – t.ex. WPF och Silverlight - så passar inte MVC-mönstret lika bra, bl.a. eftersom man då inte tar hänsyn till de kraftfulla möjligheterna till databindning som finns i dessa plattformar.

Model-View-ViewModel (M-V-VM) heter ett mönster som först beskrevs av teamet som utvecklar Expression Blend (som är ju i sig är en ganska komplex WPF-applikation). De såg behovet av att skapa en separat modell för vyn – en “ViewModel” - som ansvarar för att hantera beteende, logik och data, samt erbjuda möjlighet för vyn att databinda mot detta data. Till skillnad från Modellen så kan en ViewModel innehålla egenskaper som är specifika för användandet i ett visst gränssnitt – alltså en viss vy. T.ex. skulle en gränssnittsspecifik egenskap kunna vara ‘CanSearch’, som kan användas för att avgöra ifall en sökknapp ska vara ‘enabled’ eller ‘disabled’. Denna egenskap kan i sin tur bero på en eller flera egenskaper hos den underliggande data/domänmodellen. Liknande tankar har länge funnits bland de som sysslar med gränssnittsutveckling och M-V-VM är en variant av det mönster som Martin Fowler kallar för Presentation Model.

Om man tittar specifikt på Silverlight/WPF så innehåller M-V-VM-mönstret följande delar:

  • Model
    Representerar de objekt och de affärsregler som applikationen/systemet använder. Kan användas av flera delar av applikationen/systemet. Har ingen kännedom om vyerna och bör inte innehålla gränssnittsspecifika regler eller egenskaper. Modellen representeras vanligtvis av .NET-klasser (kan vara genererade exempelvis LINQ to SQL eller Entity Framework entiteter).  
  • View
    Användargränssnittet - deklarativt definierat i XAML, editeras i Blend/Visual Studio. Kan ägas helt av designer/interaktionsdesigner. Använder DataContext för databinding samt Commands för att kommunicera med ViewModel (mer om det nedan).  
  • ViewModel
    Vyns modell, kan ses som en abstraktion av Vyn samt en specialisering av Modellen i och med att den sitter mellan Modellen och Vyn och omformar data från Modellen till datastrukturer som Vyn kan databinda sina kontroller mot. Innehåller ‘Commands’ som används för att kommunicera från Vyn till Modellen (t.ex. klicka på “Spara” så sparas viss data som ändrats i en vy).

Ett starkt syfte med att använda det här mönstret är alltså att få bort så mycket av kod från Code Behind som möjligt för att göra applikationen mer testbar, enklare att designa och lättare att underhålla. Istället för att “limma ihop” ihop kontroller i vyn med eventhanterare i Code Behind så anropas alltså kommandon, ‘Commands’, som triggar ViewModel att utföra olika uppgifter.

Här är ett supersimpelt exempel i form av en WPF-applikation som hämtar kunder ifrån Customer-tabellen i gamla hederliga Northwind-databasen mha Entity Framework:

CustomerView

XAML-koden för min vy ser ut så här:

 <Window x:Class="ModelViewViewmodelSample.CustomerView"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="CustomerView" Height="300" Width="300">
    <Grid x:Name="grid1">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <ListBox x:Name="lb" Grid.Row="0" Grid.ColumnSpan="2" Grid.Column="0" ItemsSource="{Binding Path=Customers}" IsSynchronizedWithCurrentItem="True" Margin="5">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Path=CompanyName}" />
                        <TextBlock Text=" - " />
                        <TextBlock Text="{Binding Path=ContactName}"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <TextBlock Grid.Row="1" Grid.Column="0" Margin="5" VerticalAlignment="Center">Customer Name:</TextBlock>
        <TextBox Grid.Row="1" Grid.Column="1" Margin="5" Text="{Binding Path=Customers.CurrentItem.CompanyName}" />
        <TextBlock Grid.Row="2" Grid.Column="0" Margin="5" VerticalAlignment="Center">Contact Name:</TextBlock>
        <TextBox Grid.Row="2" Grid.Column="1" Margin="5" Text="{Binding Path=Customers.CurrentItem.ContactName}"/>
        <Button Command="{Binding Path=Update}" Grid.Row="3" Grid.Column="0"  Name="UpdateButton" Width="75" Margin="5">Update</Button>
    </Grid>
</Window>

Observera att jag inte har någon specifik eventhantering kopplad mot min knapp, endast en databindning mot ett Command vid namn ‘Update’.

Code-behind-filen är inte kliniskt ren från kod, men det räcker med en enda rad för att databinda Grid-kontrollen mot min ViewModel:

 //... ett gäng using bortklippta för att korta ner snippet

namespace ModelViewViewmodelSample
{
    /// <summary>
    /// Vyn innehåller ingen logik i code behind...
    /// </summary>
    public partial class CustomerView : Window
    {
        public CustomerView()
        {
            InitializeComponent();

            grid1.DataContext = new CustomerViewModel();

        }
    }
}

Förutom gränssnittet XAML + code behind ovan, Northwind SQL Express-databasen, samt den genererade Entity Framework-modellen, så innehåller mitt projekt även en .cs-fil som innehåller min ViewModel – CustomerViewModel samt en klass som definierar ett UpdateCommand:

 //... ett gäng using bortklippta för att korta ner snippet

namespace ModelViewViewmodelSample
{
    public class CustomerViewModel
    {
        private NorthwindEntities entities;
        private ICommand updateCommand;
        private ObjectResult<Customers> customers;

        public CustomerViewModel()
        {
            entities = new NorthwindEntities();
            ObjectQuery<Customers> query = entities.Customers.Top("5");
            customers = query.Execute(MergeOption.AppendOnly);
        }

        public NorthwindEntities Model
        {
            get
            {
                return this.entities;
            }
        }

        public ObjectResult Customers
        {
            get
            {
                return this.customers;
            }
        }

        public ICommand Update
        {
            get
            {
                if (this.updateCommand == null)
                {
                    this.updateCommand = new UpdateCommand(this);
                }
                return this.updateCommand;
            }
        }
    }

    public class UpdateCommand : ICommand
    {
        private CustomerViewModel viewModel;

        public UpdateCommand(CustomerViewModel customerViewModel)
        {
            this.viewModel = customerViewModel;
        }

        // här sparas ändringarna via Entity Framework
        public void Execute(object parameter)
        {
            this.viewModel.Model.SaveChanges();
        }
        public event EventHandler CanExecuteChanged;

        public virtual bool CanExecute(object parameter)
        {
            return true;
        }

        protected void OnCanExecuteChanged()
        {
            if (this.CanExecuteChanged != null)
            {
                this.CanExecuteChanged(this, EventArgs.Empty);
            }
        }
    }
}

Några kommentarer till min ViewModel: jag använder här Entity Framework (EF) som ett rent dataaccesslager på ett sätt som skapar ett hårt beroende till att använda just EF för dataaccess. Idealiskt skulle jag vilja abstrahera bort det till ett separat lager, inte minst för att enklare kunna mocka bort beroendet när jag enhetstestar min ViewModel. Det här är inte helt trivialt att genomföra med Entity Framework version 1, bl.a. beroende på hur dess ObjectContext fungerar - som krävs för att hålla reda på förändringar i objekten. Diego Vega har skrivit en intressant artikel om utmaningarna med att enhetstesta EF. Efter att ha konsulterat Patrik Löwendahl känns någon form av Repository-mönster som vägen att gå.

Jag kommer återkomma i en senare post, efter lite refactoring, för att försöka visa hur du trots allt hjälpligt kan enhetstesta - eller iallafall integrationstesta - det här projektet. Stay tuned!

När det gäller Silverlight så finns inte exakt samma möjlighet att använda Commands som i WPF eftersom kontrollerna inte vet hur de ska databinda mot ICommand. Julian Dominguez har visat hur man kan åstadkomma det mha Attached Behaviours. Nikhil Khotari har en annan intressant lösning på det genom att införa något han kallar för ‘Action Behaviours’. Ytterligare ett alternativ är att använda den implementation av Commands som finns i Codeplex-projektet Silverlight Extensions.

Mitt exempelprojekt kan du ladda hem här (kräver Visual Studio 2008 SP 1 och SQL Express).

Vill du läsa mer rekommenderas följande artiklar:

Dan Crevier om DataModel-View-ViewModel (i princip samma mönster som M-V-VM):

  1. DataModel-View-ViewModel pattern: 1
  2. DataModel-View-ViewModel pattern: 2
  3. DM-V-VM part 3: A sample DataModel
  4. DM-V-VM part 4: Unit testing the DataModel
  5. DM-V-VM part 5: Commands
  6. DM-V-VM part 6: Revisiting the data model
  7. DM-V-VM part 7: Encapsulating commands
  8. DM-V-VM part 8: View Models

John Gossman (som jag tror var den som myntade begreppet Model-View-ViewModel):

Comments

  • Anonymous
    January 15, 2009
    Riktigt bra skriven! :-) /Mikael Söderström

  • Anonymous
    January 20, 2009
    Äntligen någon som skriver om M-V-VM så att man förstår. /Dan

  • Anonymous
    April 04, 2009
    En av mina absoluta favoriter bland Microsofts omfattande utbud av resurser för utvecklare (samlat under

  • Anonymous
    April 20, 2009
    Några saker som jag verkligen gillar med .NET RIA Services är hur hur väl det passar in i en modern flerskiktad