IoC w XAML (proof-of-concept).

01.06.2011

Pracując z WPFem/XAMLem wielokrotnie natrafiałem na kod tego typu (bindowanie ViewModel do DataContext):

<Window x:Class="WpfApplication3.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:my="clr-namespace:WpfApplication3" 
        Height="300" Width="300">
    <Window.Resources>
        <my:WindowViewModel x:Key="viewModel"></my:WindowViewModel>
    </Window.Resources>
    <Grid  DataContext="{StaticResource viewModel}">
        <TextBlock Text="{Binding Text}"></TextBlock>
    </Grid>
</Window>

Jest to dość wygodne rozwiązanie w małych i średnich aplikacjach nie wykorzystujących zaawansowanych wzorców prezentacyjnych (MVP, MVVM itp). Jednak ma ono jedną, podstawową wadę. Specyfika XAML’a wymusza istnienie bezparametrowego konstruktora w WindowViewModel, przy użyciu którego będzie tworzony obiekt przypinany do DataContext. Tworzenie obiektów w XAML’u przy użyciu parametrycznych konstruktorów nie jest w ogóle możliwe. Problem zaczyna się gdy chcemy wstrzyknąć do naszego modelu zależności.  Jesteśmy skazani na “jeden właściwy”  konstruktor. Zostaje tylko tzw. poor man’s injection.

public class WindowViewModel
{
    private readonly IFoo _foo;

    public WindowViewModel() : this(IoC.Resolve<IFoo>())
    {
            
    }

    public WindowViewModel(IFoo foo)
    {
        _foo = foo;
    }
}

Nieciekawie to wygląda. Jednak jak “mus to mus”. Do tego problemu wróciłem ostatnio po lekturze WPF Unleashed i zapoznaniu się z klasą MarkupExtension. Ta klasa to nic innego jak wyrażenie “w klamerkce” pisane w XAML’u, czyli {Binding …}, {Static …}, {StaticResource …} itd. Dlaczego by nie zrobić własnego MarkupExtension, dzięki któremu będę mógł korzystać dobrodziejstw kontenerów IoC w XAMLu? Okazało się to prostsze niż przypuszczałem. Wystarczyło odziedziczyć po klasie MarkupExtension, stworzyć konstruktor przyjmujący typ jako parametr i dodać kod do obsługi kontenera IoC (w tym przypadku Unity, cała 1 linia):

namespace WpfApplication3
{
    public class Resolve : MarkupExtension
    {
        private readonly Type _type;

        public Resolve(Type type)
        {
            _type = type;
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return UnityContainer.Resolve(_type);
        }
    }
}

Przykładowy modelu widoku:

public class WindowViewModel
{
    private readonly IFoo _foo;

    public WindowViewModel(IFoo foo)
    {
        _foo = foo;
    }

    public string Text
    {
        get {
            return _foo.Text;
        }
    }
}

I użycie mojego Resolve w XAML’u:

<Window x:Class="WpfApplication3.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:WpfApplication3" 
        Height="300" Width="300">
    <Grid DataContext="{my:Resolve {x:Type my:WindowViewModel}}">
        <TextBlock Text="{Binding Text}"></TextBlock>
    </Grid>
</Window>

Dlaczego {my:Resolve …} zamiast {Resolve}? Zapis bez przedrostka (domyślny namespace) przyporządkowany jest do “http://schemas.microsoft.com/winfx/2006/xaml/presentation”, czyli do języka XAML. Mógłbym to zmienić i korzystać z Resolve bez przedrostka jednak musiałbym dodawać przedrostek do wszystkich definicji obiektów w XAML’u (np. <my:Window>, <my:Grid> itd.) co byłoby z oczywistych względów o wiele bardziej uciążliwe niż jednorazowe użycie {my:Resolve …}. Druga sprawa. Dlaczego {x:Resole {x:Type … i tutaj dopiero typ z przedrostkiem my…}}? Mógłbym użyć zwykłego stringa, ale pozbawiłbym się tym samym Intellisense i musiałbym lekko przerobić kod Resolve i skorzystać z Type.GetType(string name) do zamiany string na typ.

P.S. Istnieje jeszcze “tłuste rozwiązanie” w przypadku stosowania wzorca MVP i materializacji prezentera przy użyciu kontenera IoC. Jednak trzeba wtedy stworzyć trochę więcej kodu: interfejs widoku(ów) i prezentera(ów) oraz klasę bazową widoku. Materializujemy prezenter a reszta “dzieję się sama”.