WPF w połączeniu z MVVM to wspaniała sprawa. Co natomiast zrobić, jak interfejs WPF nie daje tego, co daje interfejs WinForms? W tym poście opisuję wzbogacanie kontrolek WPF.
Przykładowa aplikacja w technologii WPF + MVVM
Rozważmy następującą aplikację WPF + MVVM wykorzystującą MVVM Light Toolkit (opens new window).
<Window x:Class="Mvvm.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ignore="http://www.galasoft.ch/ignore"
xmlns:extenders="clr-namespace:Mvvm.Extenders"
mc:Ignorable="d ignore"
Height="300"
Width="300"
Title="Auto scroll demo"
DataContext="{Binding Main, Source={StaticResource Locator}}">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Skins/MainSkin.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<DockPanel>
<Button DockPanel.Dock="Top"
Content="add item"
Margin="10"
Command='{Binding AddItemClick}'/>
<ListBox Margin="10"
ItemsSource="{Binding DataForList}" />
</DockPanel>
</Window>
using System.Collections.ObjectModel;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
namespace Mvvm.ViewModel
{
public class MainViewModel : ViewModelBase
{
public RelayCommand AddItemClick { get; private set; }
public ObservableCollection<string> DataForList { get; }
public MainViewModel()
{
AddItemClick = new RelayCommand(AddNewItemToList);
DataForList =
new ObservableCollection<string> {
"item_1",
"item_2"
};
}
private void AddNewItemToList()
{
DataForList.Add($"item_{DataForList.Count+1}");
}
}
}
Chcemy, aby kontrolka Listbox automatycznie zawsze pokazywała ostatnio dodany element. Załóżmy, że wyświetlamy logi i chcemy widzieć zawsze najnowsze. Jak to zrobić?
W WinForms jest to trywialne zadanie, ale w WPF niekoniecznie. Interfejs XAML nie zawiera takiej opcji. Możemy napisać ten kawałek logiki prezentacji w code behind. Problemem jest to, że im więcej umieszczamy w code behind, tym bardziej odchodzimy od MVVM w stronę klasycznej WinForms. A my chcemy WPF + MVVM zamiast WinForms.
Rozszerzenie możliwości kontrolki ListBox
Rozszerzenie do WPF, pozwala nam dodać do kontrolek właściwości, jakich nie posiadają.
<ListBox Margin="10"
ItemsSource="{Binding DataForList}"
extenders:ListBoxExt.ScrollToLast="True"/>
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
namespace Mvvm.Extenders
{
public class ListBoxExt : DependencyObject
{
public static readonly DependencyProperty ScrollToLastProperty =
DependencyProperty.RegisterAttached("ScrollToLast",
typeof(bool),
typeof(ListBoxExt),
new UIPropertyMetadata(default(bool), OnScrollToLastChanged));
private static readonly Dictionary<object, ListBox> ListBoxMap = new Dictionary<object, ListBox>();
private static void CollectionChanged(object colection, NotifyCollectionChangedEventArgs eventArgs)
{
var listBox = ListBoxMap[colection];
var listBoxItems = listBox.Items;
if (listBox.Items.Count <= 0) return;
var lastItem = listBox.Items[listBox.Items.Count - 1];
listBoxItems.MoveCurrentTo(lastItem);
listBox.ScrollIntoView(lastItem);
}
public static void OnScrollToLastChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
{
Debug.Assert(s is ListBox, "DependencyObject is not of ListBox type");
var listBox = (ListBox) s;
var sourceCollection = listBox.Items.SourceCollection as INotifyCollectionChanged;
Debug.Assert(sourceCollection != null, "sourceCollection problem");
if ((bool) e.NewValue)
{
ListBoxMap.Add(sourceCollection, listBox);
sourceCollection.CollectionChanged += CollectionChanged;
return;
}
ListBoxMap.Remove(sourceCollection);
sourceCollection.CollectionChanged -= CollectionChanged;
}
public static bool GetScrollToLast(DependencyObject obj)
{
return (bool)obj.GetValue(ScrollToLastProperty);
}
public static void SetScrollToLast(DependencyObject obj, bool value)
{
obj.SetValue(ScrollToLastProperty, value);
}
}
}
Co widzimy na listingu powyżej:
- definiujemy klasę dziedziczącą po DependencyObject.
- określamy nazwę nowej właściwości, jej typ, typ, do jakiego obiektu możemy go podpiąć oraz metodę, jaka zostanie wywołana, gdy wartość zostanie zmieniona.
- getter oraz setter dla naszej nowej właściwości. Oczywiście wszystko są to metody statyczne.
- metoda, która wykona się, gdy nasza właściwość ulegnie zmianie. Sprawdzamy obiekt macierzysty, czy jest typu ListBox i jeśli tak, to dla kolekcji danych rejestrujemy funkcję obsługi zdarzenia zmiany w kolekcji. Ponieważ sama funkcja obsługi nie będzie miała dostępu do obiektu listy, a jedynie do skojarzonych z nią danych, to trzymamy w słowniku mapowanie.