IOCCzęść.1 Analizując ruch na swoim blogu postanowiłem zrobić wpis na temat wstrzykiwania zależności i kontenerów IOC, czyli kontenerów Inversion of Control. Dzisiaj skoncentruję się na kontenerze Castle.Windsor, chociaż w przyszłości planuję także użyć Ninject.
Co zrobię w tym wpisie? Zainstaluję Castle.Windsor z NuGet i przygotuję aplikację WPF do nowej architektury.
Wstrzykiwanie zależności : szybki skrót
Co to jest to wstrzykiwanie zależności? Za każdym razem, kiedy o tym myślę wydaje mi się, że powinienem stworzyć osobne wpisy wyjaśniające. Na chwilę obecną ich nie ma, więc szybko omówmy o co tutaj chodzi.
System odwrócenia zależności pozwala dodawać, a raczej wstrzykiwać potrzebne elementy do wykonania kodu. Zazwyczaj wstrzykiwanie zależności odbywa się przy pomocy interfejsów jak jest to ukazane poniżej.
public interface ISay
{
void Say();
}
public class Cat : ISay
{
public void Say()
{
Console.WriteLine("Meow");
}
}
public class Dog : ISay
{
public void Say()
{
Console.WriteLine("Bark");
}
}
public class Example
{
public void SaySomething(ISay someone)
{
someone.Say();
}
}
W tym kodzie jak widzisz to, co zostanie napisane będzie zależne od umieszczonej klasy w metodzie. Metoda SaySomething potrzebuje klasy, która będzie implementowała interfejs ISay. Ta metoda nie wymaga ode mnie, aby to była konkretna klasa.
class Program
{
static void Main(string[] args)
{
Example e = new Example();
e.SaySomething(new Cat());
e.SaySomething(new Dog());
}
}
Oto prosty przykład wstrzykiwania zależności przez metodę. Oczywiście tę koncepcję można zabrać dużo dalej.
public class HumanWithPet : ISay
{
private ISay _animal;
public HumanWithPet(ISay say)
{
_animal = say;
}
public void Say()
{
Console.WriteLine("Jestem człowiekiem
i oto mój zwierz");
_animal.Say();
}
}
Przykładowo mogę wstrzykiwać zależności do konstruktora i sprawić, aby ta klasa zachowywała się inaczej w zależności od tego, czy dodam kota, czy psa, którzy implementują interfejs ISay.
Wstrzykiwanie zależności : opcjonalne i konieczne
Istnieją dwa typy zależności.
Konieczne zależności są dodawane albo do konstruktora, albo jako argument do metody. Oznacza to, że nie możemy ich pominąć. Jeśli więc konieczność danego komponentu jest nam potrzebna, to warto skorzystać z tych technik.
Zależność też może być opcjonalna. W takim wypadku najczęściej korzysta się z właściwości. Przykładowo, jeśli logowanie jest mi potrzebne, to wstrzykuję do właściwości odpowiednią klasę logującą w przeciwnym wypadku domyślny logger nic nie robi.
public class Nulllogger : ILog
{
public void Write(string a)
{}
}
public class Example
{
public ILog Logger { get; set; }
public Example()
{
Logger = new Nulllogger();
}
public void DrawSomething(ISay someone)
{
someone.Say();
}
}
Oto bardzo prymitywny przykład jak to miałoby działać. Wstrzykiwanie do właściwości może się odbyć tylko, gdy właściwość jest publiczna, ma metodę SET oraz jest instancją.
public class ConsoleLogger : ILog
{
public void Write(string a)
{
Console.Write(a);
}
}
class Program
{
static void Main(string[] args)
{
Example e = new Example();
e.Logger = new ConsoleLogger();
Wstrzykiwanie oczywiście w prawdziwym świecie wygląda inaczej bo cała filozofia odwrócenia zależności polega na tym, aby w kodzie nie używać słowa kluczowego new za każdym razem, gdy potrzebuję jakiegoś obiektu.
Inversion of Control
Stworzymy więc nową aplikacje w WPF i krok po kroku przygotujmy ją
Aby IOC działało poprawnie musimy mieć jedno miejsce, w który
- zarejestrujemy zależności
- rozwiążemy zależności
- i przygotujemy metody do wywołań
Jeśli będziemy to robić w wielu miejscach to trafimy na antywzorzec zwany “Service Locator”. Ten antywzorzec informuje nas, że nie stworzyliśmy tak naprawdę systemu z odwróconą zależnością.
Jeśli miałbyś przepisać gigantyczną aplikację na IOC, to oczywiście trudno byłoby bez tego antywzorca zrobić wszystko dobrze za jednym zamachem. Poza tym, jeśli mam być z tobą szczery, bo widziałem wiele projektów, czasem trzeba skorzystać z “Service Locatora”, ale są to przypadki bardzo unikalne.
Wróćmy jednak do WPF. Po pierwsze musimy wyrzucić domyślne uruchomienie aplikacji WPF z APP.xaml.
<Application x:Class="CastleWindsorWPF.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CastleWindsorWPF"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
Wywali więc z kodu XAML atrybut “StartupUri”.
<Application x:Class="CastleWindsorWPF.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CastleWindsorWPF"
>
<Application.Resources>
</Application.Resources>
</Application>
StartupUri mówi aplikacji, która formatka powinna zostać uruchomiona najpierw. Jak widzisz referuje się ona do pliku MainWindow.xaml.
Ta formatka zostanie oczywiście uruchomiona, ale w inny sposób. Chcemy w końcu wstrzyknąć do niej zależności.
Czas więc dodać paczkę Castle.Windsor z NuGet. Kliknij na references prawym przyciskiem myszki i zaznacz opcję “Manage NuGet Packages”.
Zaznacz zakładkę “Browse” i wpisz “Castle.Windsor”. Pobierz najnowszą wersję tej paczki.
Zajrzy więc do kodu App.xaml.cs, który obecnie powinien wyglądać tak:
namespace CastleWindsorWPF
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}
Wróć do pliku App.xaml i w oknie właściwości, w zakładce zdarzeń kliknij dwa razy na zdarzenie Startup.
To powinno utworzyć metodę w pliku App.xaml.cs
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
}
Następnie do kodu dodaj kontener Windsor, który jest interfejsem IWindsorContainer. Skorzystaj z pomocy Visual Studio, aby dodać odpowiednie przestrzenie nazw.
Twój kod powinien obecnie wyglądać tak:
using Castle.Windsor;
using System.Windows;
namespace CastleWindsorWPF
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
private IWindsorContainer _container;
private void Application_Startup(object sender, StartupEventArgs e)
{
}
}
}
Aby pokazać działanie IOC w praktyce potrzebne będą nam interfejsy oraz klasy. Oto interfejs reprezentujący zawartość kontrolki Label, którego później użyjemy.
public interface ILabel
{
string Text { get; set; }
double Size { get; set; }
}
Nasza główna strona będzie posiadać dwie takie kontrolki więc potrzebujemy dwie właściwości. Oprócz reprezentacji głównej strony będzie mi potrzebna reprezentacja związku pomiędzy modelem a widokiem.
public interface IMainPageModel
{
ILabel MainLabel { get; set; }
ILabel SubLabel { get; set; }
}
public interface IMainPageViewModel
{
IMainPageModel ViewModel { get; set; }
IMainPageModel Get();
}
Oczywiście potrzebne są mi implementacje tych interfejsów. Wygląda to tak.
public class Label : ILabel
{
public string Text { get; set; }
public double Size { get; set; }
}
public class MainPageModel : IMainPageModel
{
public ILabel MainLabel { get; set; }
public ILabel SubLabel { get; set; }
}
W momencie zwracania modelu głównego ekranu dodaję odpowiednie informacje na temat moich etykietek.
public class MainPageViewModel : IMainPageViewModel
{
public IMainPageModel ViewModel { get; set; }
public MainPageViewModel() { }
public IMainPageModel Get()
{
ViewModel.MainLabel.Text = "Castle.Windsor";
ViewModel.MainLabel.Size = 36;
ViewModel.SubLabel.Text = "Inversion of Control";
ViewModel.SubLabel.Size = 16;
return ViewModel;
}
}
To jeszcze nie wszystko potrzebne są mi też główny interfejs i klasa, która uruchomi mi główne okno.
public interface IShell
{
void Run();
}
Castle stworzy za mnie instancję MainWindow. Jak widzisz Shell spodziewa się, że instancja MainWindow będzie dostarczona do konstruktora i tak będzie.
Castle jest na tyle rozumny, że rozumie nawet koncepcję nazw i automatycznie doda mi instancję głównego ekranu do właściwości window
public class Shell : IShell
{
public Shell(MainWindow window)
{
window.Title = "";
}
public virtual MainWindow window { get; set; }
public void Run()
{
window.Show();
}
}
Do szczęścia jeszcze nam jest potrzebna deklaracja, jaki interfejs będzie przez co implementowany. Zróbmy to porządnie przy pomocy instalatorów. Utwórz osobą klasę dziedziczącą po IWindsorInstaller.
public class Installers : IWindsorInstaller
{
public void Install(Castle.Windsor.IWindsorContainer container,
Castle.MicroKernel.SubSystems.Configuration.IConfigurationStore store)
{
//rejestracja próśb
container.Register(
// gdy zapytam o ILabel
Component.For<ILabel>().
//to mam dać ci to
ImplementedBy<Label>());
container.Register(Component.For<IMainPageModel>()
.ImplementedBy<MainPageModel>());
container.Register(Component.For<IMainPageViewModel>().
ImplementedBy<MainPageViewModel>());
container.Register(Component.For<IShell>()
.ImplementedBy<Shell>());
container.Register(Component.For<MainWindow>().LifestyleTransient());
}
}
Rejestracja jest bardzo prosta, jeśli złamiemy i podzielimy to wyrażenie w następujący sposób.
//rejestracja próśb
container.Register(
// gdy zapytam o ILabel
Component.For<ILabel>().
//to mam dać ci to
ImplementedBy<Label>());
Teraz wracamy do kodu App.xaml.cs. W nim mówimy Castle, aby zainstalował wszystkie instalatory z tej biblioteki. Mamy oczywiście tylko jeden instalator, ale gdybyśmy mieli ich więcej byłoby to bardzo przydatne.
Oprócz tego pobieramy od kontenera instancję IShell i przy jego pomocy uruchamiamy główne okno aplikacji.
public partial class App : Application
{
private IWindsorContainer _container;
private void Application_Startup(object sender,
StartupEventArgs e)
{
_container = new WindsorContainer();
_container.Install(FromAssembly.This());
var start = _container.Resolve<IShell>();
start.Run();
_container.Release(start);
}
}
Teraz w głównym oknie, w konstruktorze dodajmy nasz główny interfejs opakowujący model głównego ekranu.
W WPF mamy powiązania, więc nie ma co przypisywać wartości pól. Dlatego ustawmy DataContext i przejdźmy do pliku XAML.
public partial class MainWindow : Window
{
public MainWindow(IMainPageViewModel context)
{
InitializeComponent();
this.DataContext = context.Get();
}
}
W pliku XAML deklarujemy powiązania z odpowiednimi polami naszego głównego modelu.
<Window x:Class="CastleWindsorWPF.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:local="clr-namespace:CastleWindsorWPF"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Label Content="{Binding MainLabel.Text}" FontSize="{Binding MainLabel.Size}" />
<Label Content="{Binding SubLabel.Text}" FontSize="{Binding SubLabel.Size}" />
</StackPanel>
</Window>
Teraz wartości, które zadeklarowaliśmy wcześniej zostaną nam wyświetlone. Mamy nawet teraz kontrolę nad wielkością napisów.
Gdy jednak uruchomisz aplikację zobaczysz takie zonka. Dlaczego mamy dwa razy ten sam napis, skoro są dwie różne właściwości.
Problem leży w rejestracji. Wróć więc do instalatora i do rejestracji ILabel dodaj informację o cyklu życia. Domyślnie Castle uznał, że interfejs ILabel ma być singeltonem. Czyli zawszę będzie on miał jedną instancję.
Na szczęście możesz to zmienić.
//rejestracja próśb
container.Register(
// gdy zapytam o ILabel
Component.For<ILabel>().
//to mam dać ci to
ImplementedBy<Label>()
//cykl życia
.LifestyleTransient());
Teraz jak widzisz wszystko działa w porządku.
Na koniec jeszcze dodałem kod zamykający całą aplikację, bo zobaczyłem, że po zamknięciu okna proces aplikacji nadal istnieje.
public partial class MainWindow : Window
{
public MainWindow(IMainPageViewModel context)
{
InitializeComponent();
this.DataContext = context.Get();
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
Application.Current.Shutdown();
}
}
To wszystko na razie.
Edit z 2022 roku:
Kod jest do pobrania na GitHubie
PanNiebieski/CastleWindsorWithWPF (github.com)