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.
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.
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}" />
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}}" />
No Comments