Rozpoczynam nową aplikację desktopową i chcę ją zbudować przy użyciu MVVM i WPF.
Zamierzam również skorzystać z TDD.
Problem polega na tym, że nie wiem, jak powinienem użyć kontenera IoC, aby wstrzyknąć moje zależności w moim kodzie produkcyjnym.
Załóżmy, że mam następującą klasę i interfejs:
public interface IStorage
{
bool SaveFile(string content);
}
public class Storage : IStorage
{
public bool SaveFile(string content){
// Saves the file using StreamWriter
}
}
A potem mam inną klasę, która ma IStorage
jako zależność, przypuśćmy również, że ta klasa jest ViewModel lub klasą biznesową ...
public class SomeViewModel
{
private IStorage _storage;
public SomeViewModel(IStorage storage){
_storage = storage;
}
}
Dzięki temu mogę łatwo pisać testy jednostkowe, aby upewnić się, że działają poprawnie, używając makiet itp.
Problem polega na tym, że chodzi o użycie go w prawdziwej aplikacji. Wiem, że muszę mieć kontener IoC, który łączy domyślną implementację IStorage
interfejsu, ale jak mam to zrobić?
Na przykład, jak by to było, gdybym miał następujący xaml:
<Window
... xmlns definitions ...
>
<Window.DataContext>
<local:SomeViewModel />
</Window.DataContext>
</Window>
Jak poprawnie „powiedzieć” WPF, aby w takim przypadku wstrzyknął zależności?
Załóżmy również, że potrzebuję wystąpienia SomeViewModel
z mojego kodu C #, jak mam to zrobić?
Czuję, że jestem w tym całkowicie zagubiony, byłbym wdzięczny za każdy przykład lub wskazówkę, jak najlepiej sobie z tym poradzić.
Znam StructureMap, ale nie jestem ekspertem. Ponadto, jeśli istnieje lepszy / łatwiejszy / gotowy do użycia framework, daj mi znać.
źródło
Odpowiedzi:
Używałem Ninject i stwierdziłem, że praca z nim to przyjemność. Wszystko jest ustawione w kodzie, składnia jest dość prosta i ma dobrą dokumentację (i mnóstwo odpowiedzi na SO).
Zasadniczo wygląda to tak:
Utwórz model widoku i weź
IStorage
interfejs jako parametr konstruktora:Utwórz
ViewModelLocator
z właściwością get dla modelu widoku, która wczytuje model widoku z Ninject:Utwórz
ViewModelLocator
zasób dla całej aplikacji w App.xaml:Powiąż
DataContext
zUserControl
odpowiednią właściwością w ViewModelLocator.Utwórz klasę dziedziczącą NinjectModule, która skonfiguruje niezbędne powiązania (
IStorage
i model widoku):Zainicjuj jądro IoC podczas uruchamiania aplikacji za pomocą niezbędnych modułów Ninject (na razie tego powyżej):
Użyłem
IocKernel
klasy statycznej do przechowywania instancji jądra IoC obejmującej całą aplikację, dzięki czemu mogę łatwo uzyskać do niego dostęp w razie potrzeby:To rozwiązanie wykorzystuje statyczny
ServiceLocator
(theIocKernel
), który jest powszechnie uważany za anty-wzorzec, ponieważ ukrywa zależności klasy. Jednak bardzo trudno jest uniknąć pewnego rodzaju ręcznego wyszukiwania usług dla klas interfejsu użytkownika, ponieważ muszą one mieć konstruktor bez parametrów, a mimo to nie można kontrolować wystąpienia, więc nie można wstrzyknąć maszyny wirtualnej. Przynajmniej ten sposób pozwala przetestować maszynę wirtualną w izolacji, czyli tam, gdzie znajduje się cała logika biznesowa.Jeśli ktoś ma lepszy sposób, podziel się nim.
EDYCJA: Lucky Likey dostarczył odpowiedź, aby pozbyć się statycznego lokalizatora usług, pozwalając Ninject na utworzenie instancji klas interfejsu użytkownika. Szczegóły odpowiedzi można zobaczyć tutaj
źródło
DataContext="{Binding [...]}"
. Powoduje to, że VS-Designer wykonuje cały kod programu w konstruktorze ViewModel. W moim przypadku okno jest wykonywane i modalnie blokuje wszelkie interakcje z VS. Być może należy zmodyfikować ViewModelLocator, aby nie lokalizować „prawdziwych” modeli ViewModels w czasie projektowania. - Innym rozwiązaniem jest „Wyłącz kod projektu”, co również zapobiegnie wyświetlaniu wszystkiego innego. Może już znalazłeś zgrabne rozwiązanie tego problemu. W takim razie proszę o pokazanie tego.W swoim pytaniu ustawiasz wartość
DataContext
właściwości widoku w XAML. Wymaga to, aby model widoku miał domyślnego konstruktora. Jednak, jak już zauważyłeś, nie działa to dobrze w przypadku wstrzykiwania zależności, w którym chcesz wstrzyknąć zależności w konstruktorze.Dlatego nie można ustawić
DataContext
właściwości w języku XAML . Zamiast tego masz inne alternatywy.Jeśli aplikacja jest oparta na prostym hierarchicznym modelu widoku, możesz skonstruować całą hierarchię modelu widoku podczas uruchamiania aplikacji (będziesz musiał usunąć
StartupUri
właściwość zApp.xaml
pliku):Opiera się to na wykresie obiektów modeli widoku zakorzenionych w programie,
RootViewModel
ale można wstrzyknąć niektóre fabryki modeli widoku do nadrzędnych modeli widoków, umożliwiając im tworzenie nowych podrzędnych modeli widoków, dzięki czemu wykres obiektu nie musi być naprawiany. Mam nadzieję, że to również odpowiada na twoje pytanie, przypuszczam, że potrzebuję wystąpieniaSomeViewModel
z mojegocs
kodu, jak mam to zrobić?Jeśli Twoja aplikacja ma bardziej dynamiczny charakter i być może opiera się na nawigacji, będziesz musiał podłączyć się do kodu, który wykonuje nawigację. Za każdym razem, gdy przechodzisz do nowego widoku, musisz utworzyć model widoku (z kontenera DI), sam widok i ustawić
DataContext
widok na model widoku. Możesz najpierw wykonać ten widok, w którym wybierasz model widoku oparty na widoku, lub możesz to zrobić najpierw model widokugdzie model widoku określa, którego widoku użyć. Struktura MVVM zapewnia tę kluczową funkcjonalność z pewnym sposobem na podłączenie kontenera DI do tworzenia modeli widoków, ale można ją również zaimplementować samodzielnie. Jestem tutaj trochę niejasny, ponieważ w zależności od potrzeb ta funkcjonalność może stać się dość złożona. Jest to jedna z podstawowych funkcji, które uzyskujesz z frameworka MVVM, ale wprowadzenie własnej w prostej aplikacji da ci dobre zrozumienie, jakie frameworki MVVM zapewniają pod maską.Brak możliwości zadeklarowania
DataContext
w XAML powoduje utratę obsługi w czasie projektowania. Jeśli model widoku zawiera jakieś dane, pojawią się one w czasie projektowania, co może być bardzo przydatne. Na szczęście atrybutów czasu projektowania można używać również w WPF. Jednym ze sposobów jest dodanie następujących atrybutów do<Window>
elementu lub<UserControl>
w języku XAML:Typ modelu widoku powinien mieć dwa konstruktory, domyślny dla danych czasu projektowania i inny dla iniekcji zależności:
W ten sposób możesz użyć iniekcji zależności i zachować dobrą obsługę w czasie projektowania.
źródło
To, co tutaj zamieszczam, to ulepszenie odpowiedzi sondergarda, ponieważ to, co powiem, nie pasuje do komentarza :)
W rzeczywistości przedstawiam zgrabne rozwiązanie, które eliminuje potrzebę ServiceLocator i opakowania dla
StandardKernel
-Instance, które w rozwiązaniu sondergarda nazywa sięIocContainer
. Czemu? Jak wspomniano, są to anty-wzorce.Udostępnianie
StandardKernel
wszędzieKluczem do magii Ninject jest
StandardKernel
-Instance, która jest potrzebna do użycia.Get<T>()
-Method.Alternatywnie do sondergardów
IocContainer
możesz stworzyćStandardKernel
wewnętrznąApp
klasę -Class.Po prostu usuń StartUpUri z pliku App.xaml
To jest CodeBehind aplikacji w pliku App.xaml.cs
Od teraz Ninject żyje i jest gotowy do walki :)
Wstrzyknięcie
DataContext
Ponieważ Ninject żyje, możesz wykonywać wszelkiego rodzaju iniekcje, np. Property Setter Injection lub najpopularniejszy Constructor Injection .
W ten sposób można wstrzykiwać ViewModel do swoich
Window
„sDataContext
Oczywiście możesz również wstrzyknąć,
IViewModel
jeśli wykonasz odpowiednie wiązania, ale to nie jest częścią tej odpowiedzi.Bezpośredni dostęp do jądra
Jeśli potrzebujesz wywołać metody bezpośrednio w jądrze (np.
.Get<T>()
-Method), możesz pozwolić jądru wstrzyknąć sobie.Jeśli potrzebujesz lokalnej instancji jądra, możesz wstrzyknąć ją jako Właściwość.
Chociaż może to być całkiem przydatne, nie polecałbym ci tego robić. Zwróć uwagę, że obiekty wstrzyknięte w ten sposób nie będą dostępne wewnątrz Konstruktora, ponieważ zostaną wprowadzone później.
Zgodnie z tym linkiem powinieneś użyć rozszerzenia fabrycznego zamiast wstrzykiwania
IKernel
(DI Container).Sposób użycia Ninject.Extensions.Factory można również tutaj zaznaczyć na czerwono .
źródło
Ninject.Extensions.Factory
, podaj to tutaj w komentarzach a dodam więcej informacji.DependencyProperty
pole zapasowe, jak i jego metody Get i Set.Wybieram podejście „najpierw widok”, gdzie przekazuję model widoku do konstruktora widoku (w jego kodzie), który zostaje przypisany do kontekstu danych, np.
Zastępuje to podejście oparte na języku XAML.
Używam frameworka Prism do obsługi nawigacji - kiedy jakiś kod żąda wyświetlenia określonego widoku (przez „nawigację” do niego), Prism rozwiąże ten widok (wewnętrznie, używając frameworka DI aplikacji); struktura DI z kolei rozwiąże wszelkie zależności, które ma widok (model widoku w moim przykładzie), a następnie rozwiąże jego zależności i tak dalej.
Wybór frameworka DI jest prawie nieistotny, ponieważ wszystkie one zasadniczo robią to samo, tj. Rejestrujesz interfejs (lub typ) wraz z konkretnym typem, który ma utworzyć instancję framework, gdy znajdzie zależność od tego interfejsu. Dla porządku używam Castle Windsor.
Nawigacja w Pryzmacie wymaga trochę czasu, ale jest całkiem dobra, gdy już się nią zajmiesz, umożliwiając komponowanie aplikacji przy użyciu różnych widoków. Np. Możesz utworzyć „region” Prism w swoim głównym oknie, a następnie używając nawigacji Prism przełączać się z jednego widoku do innego w tym regionie, np. Gdy użytkownik wybiera pozycje menu lub cokolwiek innego.
Alternatywnie spójrz na jedną z platform MVVM, takich jak MVVM Light. Nie mam z nimi żadnego doświadczenia, więc nie mogę komentować tego, czego lubią używać.
źródło
Zainstaluj MVVM Light.
Część instalacji polega na utworzeniu lokalizatora modelu widoku. Jest to klasa, która ujawnia modele widoku jako właściwości. Funkcja pobierająca te właściwości może następnie zostać zwrócona z Twojego silnika IOC. Na szczęście MVVM light zawiera również framework SimpleIOC, ale jeśli chcesz, możesz podłączyć inne.
Dzięki prostemu IOC rejestrujesz implementację dla typu ...
W tym przykładzie model widoku jest tworzony i przekazywany obiekt dostawcy usług zgodnie z jego konstruktorem.
Następnie tworzysz właściwość, która zwraca instancję z IOC.
Sprytną częścią jest to, że lokalizator modelu widoku jest następnie tworzony w pliku app.xaml lub równoważnym jako źródło danych.
Możesz teraz powiązać z jego właściwością „MyViewModel”, aby pobrać Viewmodel z wstrzykniętą usługą.
Mam nadzieję, że to pomoże. Przepraszamy za nieścisłości kodu zakodowanego z pamięci na iPadzie.
źródło
GetInstance
aniresolve
poza nim. O to chodzi w DI!Obudowa Canonic DryIoc
Odpowiadając na stary post, ale robiąc to z
DryIoc
tym, co myślę, jest dobrym wykorzystaniem DI i interfejsów (minimalne użycie konkretnych klas).App.xaml
, i tam mówimy, jaki jest pierwotny widok do użycia; robimy to z kodem za zamiast domyślnego xaml:StartupUri="MainWindow.xaml"
w App.xamlw codebehind (App.xaml.cs) dodaj to
override OnStartup
:to jest punkt startowy; to także jedyne miejsce, do którego
resolve
należy dzwonić.korzeń konfiguracji (zgodnie z książką Marka Seemana Dependency injection in .NET; jedyne miejsce, w którym należy wspomnieć o konkretnych klasach) będzie w tym samym kodzie, w konstruktorze:
Uwagi i kilka dodatkowych szczegółów
MainWindow
;Konstruktor ViewModel z DI:
Domyślny konstruktor ViewModel do projektowania:
Kod za widokiem:
i co jest potrzebne w widoku (MainWindow.xaml), aby uzyskać wystąpienie projektu z ViewModel:
Wniosek
W ten sposób otrzymaliśmy bardzo czystą i minimalną implementację aplikacji WPF z kontenerem DryIoc i DI, zachowując jednocześnie możliwe wystąpienia projektowe widoków i modeli widoków.
źródło
Użyj zarządzanej struktury rozszerzalności .
Ogólnie rzecz biorąc, należy mieć klasę statyczną i użyć wzorca fabryki, aby zapewnić globalny kontener (buforowany, natch).
Jeśli chodzi o wstrzykiwanie modeli widoku, wstrzykujesz je w ten sam sposób, w jaki wstrzykujesz wszystko inne. Utwórz konstruktora importującego (lub umieść instrukcję importu we właściwości / polu) w kodzie źródłowym pliku XAML i powiedz mu, aby zaimportował model widoku. Następnie powiązać
Window
„sDataContext
do tej nieruchomości. Twoje obiekty główne, które faktycznie wyciągasz z pojemnika, są zwykleWindow
obiektami złożonymi . Po prostu dodaj interfejsy do klas okien i wyeksportuj je, a następnie pobierz z katalogu jak powyżej (w App.xaml.cs ... to jest plik bootstrap WPF).źródło
new
.Sugerowałbym użycie ViewModel - pierwsze podejście https://github.com/Caliburn-Micro/Caliburn.Micro
zobacz: https://caliburnmicro.codeplex.com/wikipage?title=All%20About%20Conventions
używać
Castle Windsor
jako kontenera IOC.Wszystko o konwencjach
Jedną z głównych cech Caliburn.Micro jest jego zdolność do wyeliminowania konieczności stosowania kodu płyty kotłowej poprzez działanie na szeregu konwencji. Niektórzy kochają konwencje, a inni ich nienawidzą. Dlatego konwencje CM są w pełni konfigurowalne i można je nawet całkowicie wyłączyć, jeśli nie jest to pożądane. Jeśli zamierzasz używać konwencji, a ponieważ są one domyślnie WŁĄCZONE, dobrze jest wiedzieć, jakie są te konwencje i jak działają. To jest temat tego artykułu. Wyświetl rozdzielczość (najpierw model widoku)
Podstawy
Pierwsza konwencja, z którą możesz się spotkać podczas korzystania z CM, dotyczy rozdzielczości widoku. Ta konwencja ma wpływ na wszystkie obszary ViewModel-First w aplikacji. W ViewModel-First mamy istniejący ViewModel, który musimy wyrenderować na ekranie. Aby to zrobić, CM używa prostego wzorca nazewnictwa, aby znaleźć UserControl1, który powinien powiązać z ViewModel i wyświetlić. Więc co to za wzór? Wystarczy spojrzeć na ViewLocator.LocateForModelType, aby dowiedzieć się:
Najpierw zignorujmy zmienną „kontekst”. Aby uzyskać widok, zakładamy, że używasz tekstu „ViewModel” w nazewnictwie maszyn wirtualnych, więc po prostu zmieniamy go na „View” wszędzie tam, gdzie go znajdziemy, usuwając słowo „Model”. Ma to wpływ na zmianę zarówno nazw typów, jak i przestrzeni nazw. Więc ViewModels.CustomerViewModel stałoby się Views.CustomerView. Lub jeśli organizujesz swoją aplikację według funkcji: CustomerManagement.CustomerViewModel staje się CustomerManagement.CustomerView. Miejmy nadzieję, że to całkiem proste. Gdy już mamy nazwę, szukamy typów o tej nazwie. Przeszukujemy dowolny zestaw, który udostępnisz CM jako możliwy do przeszukiwania za pośrednictwem AssemblySource.Instance.2 Jeśli znajdziemy typ, tworzymy wystąpienie (lub pobieramy je z kontenera IoC, jeśli jest zarejestrowany) i zwracamy je do obiektu wywołującego. Jeśli nie znajdziemy typu,
Wróćmy teraz do wartości „kontekstu”. W ten sposób CM obsługuje wiele widoków w tym samym modelu widoku. Jeśli podano kontekst (zazwyczaj ciąg znaków lub wyliczenie), dokonujemy dalszej transformacji nazwy na podstawie tej wartości. Ta transformacja skutecznie zakłada, że masz folder (przestrzeń nazw) dla różnych widoków, usuwając słowo „Widok” z końca i dołączając zamiast tego kontekst. Tak więc, biorąc pod uwagę kontekst „Master”, nasz ViewModels.CustomerViewModel stałby się Views.Customer.Master.
źródło
Usuń startowy identyfikator URI z pliku app.xaml.
App.xaml.cs
Teraz możesz użyć swojej klasy IoC do konstruowania instancji.
MainWindowView.xaml.cs
źródło
GetInstance
zresolve
zewnętrznym plikiem app.xaml.cs, tracisz punkt DI. Ponadto wspominanie o widoku XAML w za kodem widoku jest trochę zawiłe. Po prostu wywołaj widok w czystym języku C # i zrób to z kontenerem.