Artykuł

freeimages.com freeimages.com
paź 10 2015
0

Implementacja komendy Tap dla TextBlocka

Domyślnie tworząc aplikacje uniwersalne, możemy korzystać z rozbudowanego modelu zdarzeń. Każde ze zdarzeń, może zostać zaimplementowane w tzw. części code-behind wybranej strony/kontrolki. Nie wszystkim do końca odpowiada jednak ten model. Spore grono programistów jest zwolennikiem architektury MVVM, która promuje wydzielone konstrukcje zwane ViewModelami. Ważnym elementem ViewModel są komendy, które pozwalają na realizację określonych zadań. Niestety nie wszystkie kontrolki Microsoftu są do tego dobrze przygotowane. Czy można sobie z tym jakoś poradzić?

Częściowo tak. Po pierwsze, można wykorzystać Behaviors SDK (więcej na ten temat pisałem tutaj). Jeśli to rozwiązanie nie zadziała, można skorzystać z innych metod alternatywnych - np. z Attached properties. I to właśnie ten sposób mam zamiar Wam dzisiaj zaprezentować.

Attached properties

Właściwości typu Attached są stosunkowo rzadko używane przez programistów, mimo. że oferują całkiem niezłe możliwości. Ich głównym zastosowaniem jest rozszerzenie funkcjonalności istniejących kontrolek/elementów o nowe możliwości. Jednym z najlepszych przykładów ich wykorzystania, jest kontrolka Grid i jej właściwości typu Grid.Row, czy Grid.Column itp. (więcej na ten temat pisałem tutaj).

Każdy deweloper może rozszerzyć w ten sposób funkcjonalność dowolnej kontrolki. Takie rozwiązania świetnie przydają się właśnie w implementacji MVVM, tam gdzie nie zostało to przewidziane przez programistów Microsoftu. W dalszej części tekstu, zaprezentuję prosty sposób na dodanie komendy typu Tap do kontrolki TextBlock (domyślnie obsługuje ona zdarzenie tego typu (Tapped).

Klasa TextBlockAddons

Punktem wyjścia całego przypadku będzie klasa TextBlockAddons, której kod źródłowy znajduje się poniżej:

public static class TextBlockAddons
{
	private static readonly DependencyProperty tapCommandProperty =
		DependencyProperty.RegisterAttached("TapCommand", typeof(ICommand),
		typeof(TextBlockAddons), new PropertyMetadata(null, OnCommandPropertyChanged));

	public static DependencyProperty TapCommandProperty
	{
		get
		{
			return tapCommandProperty;
		}
	}

	public static void SetTapCommand(DependencyObject d, ICommand value)
	{
		d.SetValue(TapCommandProperty, value);
	}

	public static ICommand GetTapCommand(DependencyObject d)
	{
		return (ICommand)d.GetValue(TapCommandProperty);
	}

	private static void OnCommandPropertyChanged(DependencyObject d,
		DependencyPropertyChangedEventArgs e)
	{
		//Bind event only in case of first change
		if(e.NewValue == null || e.OldValue != null)
		{
			return;
		}
		
		var textBlock = d as TextBlock;
		if (textBlock != null)
		{
			textBlock.Tapped += (object sender, TappedRoutedEventArgs et) =>
			{
				et.Handled = true;
				var command = GetTapCommand(textBlock);
				if(command != null)
				{
					command.Execute(null);
				}
			};
		}
	}
}

Pominę sam proces tworzenia właściwości (jest to standardowa procedura z wykorzystaniem metod Set... oraz Get... do ustawienia i pobierania wartości) i skupię się na statycznej metodzie OnCommandPropertyChanged (25-47).

Na początku, sprawdzamy czy nowa wartość nie jest Nullem, bądź też czy stara wartość jest różna od Nulla. Z założenia chcemy wykonać pozostałe akcje zawarte w tej metodzie tylko wtedy, gdy komenda podpinana jest po raz pierwszy. Oczywiście obecny mechanizm nie zabezpiecza przed sytuacja gdy użytkownik przypnie komendę, później ją usunie, a później przypnie kolejną. Jeśli może dojść do takiej sytuacji, to w takim przypadku warto dodać inny Attached property, który będzie działał na zasadzie jestem ustawiony bądź też nie. W przypadku gdy będzie on ustawiony, to nie powinniśmy pozwolić na stworzenie nowego zdarzenia.

W dalszej części metody pobieramy obiekt TextBlocka (opcjonalnie można zapiąć się na FrameworkElement), a następnie do zdarzenia Tapped podpinamy nasz event handler wewnątrz którego koniecznie oznaczamy zdarzenie jako obsłużone (Handled), a następnie pobieramy komendę za pomocą statycznej metody GetTapCommand. Oczywiście sprawdzamy czy przypisana komenda jest różna od Nulla.

Jedyną wadą tego modelu, jest potencjalne ryzyko ponownego utworzenia zdarzenia, dlatego też trzeba dobrze zabezpieczyć ten element, o czym pisałem wyżej.

RelayCommand

W przykładzie konieczne będzie użycie wybranej implementacji jakiejś komendy. Może być to najprostszy RelayCommand, który często można znaleźć w projektach Universal Apps:

public class RelayCommand : ICommand
{
    private readonly Action _execute;
    private readonly Func<bool> _canExecute;

    public event EventHandler CanExecuteChanged;

    public RelayCommand(Action execute)
        : this(execute, null)
    {
    }

    public RelayCommand(Action execute, Func<bool> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");
        _execute = execute;
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute();
    }

    public void Execute(object parameter)
    {
        _execute();
    }

    public void RaiseCanExecuteChanged()
    {
        var handler = CanExecuteChanged;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }
}

W naszym przypadku nie będziemy korzystać z funkcji CanExecute, aczkolwiek jest to oczywiście jak najbardziej możliwe;-)

Testowa strona

Poniżej kod testowej strony. Dla uproszczenia, kod definiujący komendę został umieszczony w części Code-behind. Nie chciałem komplikować przykładu i na siłę wprowadzać wszystkie elementy MVVM - nie taki był cel tego tekstu. Poniżej kod XAML:

<Page
    x:Class="App3.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App3"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid>
        <TextBlock VerticalAlignment="Center" FontSize="30"
                   local:TextBlockAddons.TapCommand="{Binding TextBlockTapCommand}"
                   HorizontalAlignment="Center">Test tapa</TextBlock>
    </Grid>
</Page>

Założyłem że statyczna klasa TextBlockAddons znajduje się w tej samej przestrzeni nazw co strona. Jeśli będzie inaczej, to trzeba oczywiście dodać nowy prefiks dla interesującej nas przestrzeni.

Poniżej code-behind strony MainPage:

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();

        this.TextBlockTapCommand = new RelayCommand(async () =>
        {
            var msgDlg = new MessageDialog("Test");
            await msgDlg.ShowAsync();
        });

        this.DataContext = this;
    }

    public ICommand TextBlockTapCommand { get; private set; } 
}

W kodzie definiujemy właściwość komendy, którą ustawiamy w konstruktorze klasy.

Powyższy przykład to oczywiście duże uproszczenie. W przypadku niektórych klas, istnieje możliwość ich rozszerzenia, dzięki czemu możemy wprowadzić kod odpowiedzialny za komendy bezpośrednio do implementacji, z pominięciem Attached properties. W praktyce, sporo klas wykorzystywanych w Universal Apps, zostało oznaczonych słowkiem kluczowym Sealed, które blokuje nas przed ich rozszerzaniem.

W przypadku wykorzystania AttachedProperties, nie musimy się również zapinać na konkretne kontrolki. W zależności od rodzaju zdarzenia, istnieje możliwość podpięcia się wyżej w drzewie obiektów. W moim odczuciu jest to bardziej eleganckie rozwiązanie niż choćby mieszanie modelu zdarzeniowego i komend bezpośrednio w code-behind strony.

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus