Skip to main content

MVVM Light Funktionen

Funktionen

ViewModel

ObservableCollection aus anderem Thread verändern

Das Bearbeiten von ObservableCollections aus einem anderen Thread heraus ist nicht möglich. MVVM Light bietet die Möglichkeit Actions im GUI-Thread zu invoken.

protected void InvokeGuiAction(Action callback)
{
    try
    {
        DispatcherHelper.UIDispatcher.Invoke(callback);
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

Messenger

Das Übergeben von Daten an andere Windows / UserControls oder unabhängigen Klassen, erfolgt bei MVVM nicht über direktes Ansprechen der Variablen / Methoden. Ein von MVVMLight interner Messenger-Dienst übernimmt die Daten und leitet sie an die abonnierte Methode weiter.

Empfangen von Messages

Das Empfangen von Messages erfolgt nach dem registrieren der Parameter-Klasse und einer Empfänger-Methode. Liegt die Methode in einer Klasse, die von ViewModelBase abgeleitet wurde, erfolgt der Aufruf über MessengerInstance.

public NewWindowViewModel()
{
    MessengerInstance.Register<NewWindowParameterMessage>(this, NewWindowParameterMessageReceiver);
}

Das deabonnieren erfolgt über die Methode Unregister.

private void NewWindowParameterMessageReceiver(NewWindowParameterMessage message)
{
    MessengerInstance.Unregister<NewWindowParameterMessage>(this, NewWindowParameterMessageReceiver);
}

Soll eine Message in einer Methode in einer Klasse empfangen werden, die nicht von ViewModelBase abgeleitet wurde, so erfolgt der Aufruf über die entsprechende statische Klasse Messenger.

Im Folgenden das Abo und Deabo.

public NewWindowViewModel()
{
    Messenger.Default.Register<NewWindowParameterMessage>(this, NewWindowParameterMessageReceiver);
}
 
private void NewWindowParameterMessageReceiver(NewWindowParameterMessage message)
{
    Messenger.Default.Unregister<NewWindowParameterMessage>(this, NewWindowParameterMessageReceiver);
}

Senden von Messages

Dieselbe Klasse enthält auch die Methoden zum Senden von Messages.
Auch hier gilt: Soll aus einer Methode gesendet werden, dessen Klasse nicht von ViewModelBase abgeleitet wurde, wird Messenger.Default genutzt.

NewWindowParameterMessage message = new NewWindowParameterMessage();
 
MessengerInstance.Send(message);

oder

NewWindowParameterMessage message = new NewWindowParameterMessage();
 
Messenger.Default.Send(message);

ViewModelLocator

Create new Window / UserControl

Leider bietet MVVM Light keine allgemeine Klasse zum Öffnen von neuen Fenstern. Anbei ein generisches Beispiel.
Nehmen wird an, das neue UserControl soll "Login" heißt. Im ViewModelLocator wird eine neue Property "Login" erzeugt, sowie die Registrierung des neuen ViewModels "LoginViewModel" durchgeführt.

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Ioc;
using Microsoft.Practices.ServiceLocation;
 
namespace TeamHub.Client.ViewModel
{
    public class ViewModelLocator
    {
        static ViewModelLocator()
        {
            ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
 
            if (ViewModelBase.IsInDesignModeStatic)
            {
                SimpleIoc.Default.Register<IDataService, DesignDataService>();
            }
            else
            {
                SimpleIoc.Default.Register<IDataService, DataService>();
            }
 
            SimpleIoc.Default.Register<MainViewModel>();
            // Registrierung des neuen ViewModels
            SimpleIoc.Default.Register<LoginControlViewModel>();
        }
 
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
            Justification = "This non-static member is needed for data binding purposes.")]
        public MainViewModel Main { get { return ServiceLocator.Current.GetInstance<MainViewModel>(); } }
 
        // Neue Variable für ViewModel
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
            Justification = "This non-static member is needed for data binding purposes.")]
        public LoginViewModel Login { get { return ServiceLocator.Current.GetInstance<LoginViewModel>(); } }
 
        public static void Cleanup()
        {
        }
    }
}

Anschließend wird über das Menü ein neues UserControl oder Window mit dem Namen LoginControl erstellt.

grafik.png

Das XAML muss angepasst werden. Zu beachten ist, dass das Binding im DataContext mit dem Namen der Variable (Login) im ViewModelLocator überein stimmt.

<UserControl x:Class="MyAPp.Controls.LoginControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:MyApp.Controls"
             xmlns:ignore="http://www.galasoft.ch/ignore"
             mc:Ignorable="d ignore"
             Height="399.9" Width="644.5"
             DataContext="{Binding Login, Source={StaticResource Locator}}">
 
    <Grid>
             
    </Grid>
</UserControl>

Anschließend wird das neue ViewModel mit dem Namen "LoginViewModel" angelegt.

grafik.png

Der Inhalt der Klasse LoginViewModel.

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Security;
 
namespace MyApp.ViewModel
{
    public class LoginViewModel : ViewModelBase
    {
        public LoginViewModel()
        {
        }
 
        public override void Cleanup()
        {
            // Clean up if needed
 
            base.Cleanup();
        }
    }
}

Open New Windows

Das Öffnen von Windows kann über folgende Methode in die ViewModelLocator-Klasse gekapselt werden.

public static string OpenNewWindow<TWindow, TParameter>(TParameter parameter) where TWindow : Window
{
    var uKey = Guid.NewGuid().ToString();
    var win = Activator.CreateInstance<TWindow>();
 
    if (parameter != null)
    {
        Messenger.Default.Send(parameter);
    }
 
    win.Closed += (sender, args) => SimpleIoc.Default.Unregister(uKey);
    win.ShowDialog();
 
    return uKey;
}

Der Aufruf der OpenNewWindow Methode wie folgt.

NewWindowParameterMessage parameter = new NewWindowParameterMessage();
 
string uKey = ViewModelLocator.OpenNewWindow<NewWindowWindow, NewWindowParameterMessage>(parameter);

Als Rückgabe erhält man einen Unique-Key, um mit dem neuen Fenster weitere Daten austauschen zu können.

Die Übergabe von Parametern erfolgt über den internen Messenger. Im Konstruktor des ViewModels wird die Klasse NewWindowParameterMessage als Empfänger registriert. Sobald die Daten empfangen wurde, kann die Registrierung entfernt werden. Sie wird bei jedem Fenster-Öffnen neu erzeugt.

// Constructor
public NewWindowViewModel()
{
    MessengerInstance.Register<NewWindowParameterMessage>(this, NewWindowParameterMessageReceiver);
}
 
// Receiver-Methode
private void NewWindowParameterMessageReceiver(NewWindowParameterMessage message)
{
    MessengerInstance.Unregister<NewWindowParameterMessage>(this, NewWindowParameterMessageReceiver);
}

Binding

Commands

Das Ausführen von Code über einen Button erfolgt im ViewModel.

ViewModel:

private RelayCommand _command;
 
public RelayCommand Command
{
    get
    {
        return _command
            ?? (_command = new RelayCommand(
            () =>
            {
                MessageBox.Show("Hallo");
            },
            () => CanExecuteCommand()));
    }
}
 
private bool CanExecuteCommand()
{
    return true;
}

XAML:

<Button content="Klick mich" Command="{Binding Command}" />

Soll der Button einen Wert, z. B. das SelectedItem einer Auswahlliste, übertragen, muss die Command-Methode einen Parameter empfangen.
Anstelle des object-Types kann natürlich auch bereits der SelectedItem-Type genutzt werden, um ein späteres Casting vorzubeugen.

ViewModel:

private RelayCommand<object> _command;
 
public RelayCommand<object> Command
{
    get
    {
        return _command
            ?? (_command = new RelayCommand<object>(
            parameter =>
            {
                MessageBox.Show(parameter.GetType().ToString());
            },
            parameter => CanExecuteCommand(parameter)));
    }
}
 
private bool CanExecuteCommand(object parameter)
{
    return true;
}

XAML:

<Button content="Klick mich" Command="{Binding Command}" CommandParameter="{Binding SelectedItem, ElementName=CbAuswahl}" />

Events

Das Werfen und Verarbeiten von Events erfolgt ebenfalls über Command-Properties.
Wird im Event die Property PassEventArgsToCommand auf true gesetzt, so kann das Command auf den Parent und somit auf das DataGrid, zugreifen. Darüber ist es möglich, das SelectedItem abzugreifen.

ViewModel:

private RelayCommand<MouseButtonEventArgs> _dataGridMouseDoubleClickEventCommand;
 
public RelayCommand<MouseButtonEventArgs> DataGridMouseDoubleClickEventCommand
{
    get
    {
        return _dataGridMouseDoubleClickEventCommand
            ?? (_dataGridMouseDoubleClickEventCommand = new RelayCommand<MouseButtonEventArgs>(
            p =>
            {
                DataGrid dg = p.Source as DataGrid;
 
                DataGridItem item = dg.SelectedItem as DataGridItem;
                if (item == null)
                {
                    return;
                }
 
                ...
            }));
    }
}

XAML:

...
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
        xmlns:command="http://www.galasoft.ch/mvvmlight"
...
 
 
<DataGrid AutoGenerateColumns="False"
          CanUserAddRows="False"
          CanUserDeleteRows="False"
          CanUserResizeColumns="True"
          SelectionMode="Extended"
          SelectionUnit="FullRow">
 
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="MouseDoubleClick">
      <command:EventToCommand Command="{Binding Path=DataGridMouseDoubleClickEventCommand, Mode=OneWay}"
                              PassEventArgsToCommand="True"/>
    </i:EventTrigger>
  </i:Interaction.Triggers>
 
  <DataGrid.Columns>
    <DataGridTextColumn Header="Wert" Binding="{Binding Wert}" />
  </DataGrid.Columns>
</DataGrid>

Events im TreeView-Control

Events im TreeView-Control müssen bei MVVM Light auf andere Weise abgefangen werden.

Klasse Behaviours:

Diese Klasse wird für die Bindung, des Events im CategoryItemModel, im XAML vorgesehen.

namespace MyApp.Common
{
  public static class Behaviours
  {
      public static readonly DependencyProperty ExpandingBehaviourProperty =
          DependencyProperty.RegisterAttached("ExpandingBehaviour", typeof(RelayCommand<RoutedEventArgs>), typeof(Behaviours),
              new PropertyMetadata(OnExpandingBehaviourChanged));
 
      public static void SetExpandingBehaviour(DependencyObject o, RelayCommand<RoutedEventArgs> value)
      {
          o.SetValue(ExpandingBehaviourProperty, value);
      }
 
      public static ICommand GetExpandingBehaviour(DependencyObject o)
      {
          return (ICommand)o.GetValue(ExpandingBehaviourProperty);
      }
 
      private static void OnExpandingBehaviourChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
      {
          TreeViewItem tvi = d as TreeViewItem;
          if (tvi != null)
          {
              RelayCommand<RoutedEventArgs> ic = e.NewValue as RelayCommand<RoutedEventArgs>;
              if (ic != null)
              {
                  tvi.Expanded += (s, a) =>
                  {
                      if (ic.CanExecute(a))
                      {
                          ic.Execute(a);
                      }
                      a.Handled = true;
                  };
              }
          }
      }
   }
}

ViewModel:

public class CategoryViewModel : BaseViewModel
{
    private readonly ObservableCollection<CategoryItemModel> _itemsContainer = new ObservableCollection<CategoryItemModel>();
 
    public CategoryViewModel()
    {
    }
 
    public ObservableCollection<CategoryItemModel> ItemsContainer { get { return _itemsContainer; } }
}

TreeViewItem > CategoryItemModel:

Die Property für das Event wird hierfür in der TreeViewItem-Klasse untergebracht. Es empfiehlt sich ein eigenes Item anzulegen.

public class CategoryItemModel
{
    public string Name { get; set; }
    public ObservableCollection<CategoryItemModel> Nodes { get; set; }
    private RelayCommand<RoutedEventArgs> _itemExpandedEvent;
 
    public CategoryItemModel()
    {
        Nodes = new ObservableCollection<CategoryItemModel>();
    }
 
    public RelayCommand<RoutedEventArgs> ItemExpandedEvent
    {
        get
        {
            return _itemExpandedEvent ?? (_itemExpandedEvent = new RelayCommand<RoutedEventArgs>(
                parameter =>
                {
                }));
        }
    }
}

XAML:

Der ItemsSource des TreeViews wird mit der ItemsContainer-Property im ViewModel gebunden. Der ItemsSource des HirarchicalDataTemplates wird mit der Nodes-Property im CategoryItemModel gebunden.

...
             xmlns:common="clr-namespace:MyApp.Common"
...
 
<TreeView ItemsSource="{Binding ItemsContainer}">
    <TreeView.Resources>
        <Style TargetType="TreeViewItem">
            <Setter Property="common:Behaviours.ExpandingBehaviour" Value="{Binding ItemExpandedEvent, Mode=OneWay}"/>
        </Style>
    </TreeView.Resources>
 
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate DataType="{x:Type TreeViewItem}" ItemsSource="{Binding Nodes}">
            <TextBlock Text="{Binding Name}" />
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

Properties

Properties werden zur uni- oder bidirektionalen Übertragung von Daten zwischen XAML und ViewModel benötigt.

OneWay wird benutzt, wenn das Control nur den Wert anzeigen, aber nicht verändern soll.
TwoWay wird benutzt, wenn es sich beim Control um eine TextBox handelt und der Anwender die Möglichkeit haben soll, den Wert ändern zu können.

Achtung: Es kommt schnell zu Verwirrungen, wenn der TextBox die Mode OneWay zugewiesen wird, weshalb Datenänderungen nicht im ViewModel übernommen werden!

ViewModel:

private bool _newProperty = true;
 
public bool NewProperty
{
    get { return _newProperty; }
    set { Set(() => NewProperty, ref _newProperty, value); }
}

XAML:

<Label Content="{Binding NewProperty, Mode=OneWay}" />
 
<TextBox Text="{Binding NewProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

Property Update

Ist es nötig, die GUI aufgrund von Werteänderungen der Properties zu informieren, z. B. wenn der Wert einer Property von einer anderen Property abhängt, muss dies bekannt gemacht werden.

In diesem Beispiel hängt der Wert der Property Thumbnail von der Property VideoItem ab. Nach der Zuweisung eines Wertes zu VideoItem, wird die Methode RaisePropertyChanged aufgerufen. Anschließend wird im XAML der neue Werte von Thumbnail angezeigt.

public string Thumbnail
{
    get
    {
        if (VideoItem == null ||
            VideoItem.Video == null)
        {
            return "/Images/missing_picture.png";
        }
 
        return VideoItem.Video.Thumbnail;
    }
}
 
private YoutubeContainer _videoItem = null;
 
public YoutubeContainer VideoItem
{
    get { return _videoItem; }
    set
    {
        Set(() => VideoItem, ref _videoItem, value);
 
        RaisePropertyChanged(() => Thumbnail);
    }
}

Command CanExecute Update

Ist das CanExecute eines Buttons an mehrere Properties gebunden, so muss die Command-Property aktualisiert werden. Jedes Command besitzt die Methode RaiseCnaExecuteChanged(). Wird diese Aufgerufen, wird im Hintergrund die IsEnabled-Property des XAML-Buttons überprüft. Der Aufruf kann, z. B. in der set-Methode einer Property, abgelegt werden und auf die Zuweisung von Daten reagieren.

LoginCommand.RaiseCanExecuteChanged();

Beispiele

ComboBox

Wird die aktuelle Auswahl einer ComboBox benötigt, so bindet man die Property SelectedItem an eine Property im ViewModel.

ViewModel:

private ListModel[] _listItems;
 
public ListModel[] ListItems
{
    get { return _listItems; }
}
 
private FormatModel _selectedItem;
 
public FormatModel SelectedItem
{
    get { return _selectedItem; }
    set { Set(() => SelectedItem, ref _selectedItem, value); }
}

XAML:

<ComboBox ItemsSource="{Binding ListItems, Mode=OneWay}"
          SelectedItem="{Binding SelectedItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

Fenster schließen

Ist die Bearbeitung in einem neuen Fenster beendet oder soll das Ergebnis an das Parent-Fenster übergeben werden, erfolgt dies per Übergabe des Ancestors an die vom Button aufgerufene Command-Methode.

ViewModel:

private RelayCommand<Window> _saveCommand;
 
public RelayCommand<Window> SaveCommand
{
    get
    {
        return _saveCommand
            ?? (_saveCommand = new RelayCommand<Window>(
            parameter =>
            {
                ...
 
                Window win = parameter;
                win.Close();
            }));
    }
}

XAML:

<Button Content="Speichern"
        Command="{Binding SaveCommand}"
        CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}" />