Jak obsługiwać dependency injection w aplikacji WPF / MVVM

Uruchamiam nową aplikację desktopową i chcę ją zbudować przy użyciu MVVM i WPF.

Zamierzam również użyć TDD.

Problem polega na tym, że nie wiem, jak powinienem używać kontenera IoC do wprowadzania zależności od kodu produkcyjnego.

Załóżmy, że mam klasę folowing i interfejs:

public interface IStorage
{
    bool SaveFile(string content);
}

public class Storage : IStorage
{
    public bool SaveFile(string content){
        // Saves the file using StreamWriter
    }
}

I wtedy mam inną klasę, która ma IStorage jako zależność, Załóżmy również, że ta klasa jest Viewmodelem lub klasą biznesową...

public class SomeViewModel
{
    private IStorage _storage;

    public SomeViewModel(IStorage storage){
        _storage = storage;
    }
}

Z tym I może łatwo pisać testy jednostkowe, aby upewnić się, że działają prawidłowo, za pomocą mocks i itp.

Problem jest, jeśli chodzi o użycie go w prawdziwej aplikacji. Wiem, że muszę mieć kontener IoC, który łączy domyślną implementację interfejsu IStorage, ale jak to zrobić?

Na przykład, jak by to było gdybym miał następujące xaml:

<Window 
    ... xmlns definitions ...
>
   <Window.DataContext>
        <local:SomeViewModel />
   </Window.DataContext>
</Window>

Jak poprawnie "powiedzieć" WPF, aby wstrzykiwał zależności W takim przypadku?

Również, Załóżmy, że potrzebuję przykład SomeViewModel z mojego kodu cs, Jak mam to zrobić?

Czuję się całkowicie zagubiony w tym, byłbym wdzięczny za każdy przykład lub wskazówki, jak najlepiej sobie z tym poradzić.

Znam się na StructureMap, ale nie jestem ekspertem. Ponadto, jeśli istnieje lepszy/łatwiejszy/gotowy do użycia framework, daj mi znać.

Z góry dzięki.
Author: Fedaykin, 2014-08-18

8 answers

Używałem Ninject i odkrył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).

Więc w zasadzie to idzie tak:

Utwórz model widoku i weź interfejs IStorage jako parametr konstruktora:

class UserControlViewModel
{
    public UserControlViewModel(IStorage storage)
    {

    }
}

Utwórz ViewModelLocator z właściwością get dla modelu widoku, która ładuje model widoku z Ninject:

class ViewModelLocator
{
    public UserControlViewModel UserControlViewModel
    {
        get { return IocKernel.Get<UserControlViewModel>();} // Loading UserControlViewModel will automatically load the binding for IStorage
    }
}

Make the ViewModelLocator szeroki zasób aplikacji w aplikacji.xaml:

<Application ...>
    <Application.Resources>
        <local:ViewModelLocator x:Key="ViewModelLocator"/>
    </Application.Resources>
</Application>

Powiązać DataContext z UserControl do odpowiedniej właściwości w ViewModelLocator.

<UserControl ...
             DataContext="{Binding UserControlViewModel, Source={StaticResource ViewModelLocator}}">
    <Grid>
    </Grid>
</UserControl>

Utwórz klasę dziedziczącą NinjectModule, która ustawi niezbędne powiązania (IStorage i viewmodel):

class IocConfiguration : NinjectModule
{
    public override void Load()
    {
        Bind<IStorage>().To<Storage>().InSingletonScope(); // Reuse same storage every time

        Bind<UserControlViewModel>().ToSelf().InTransientScope(); // Create new instance every time
    }
}

Zainicjalizuj jądro IoC przy starcie aplikacji niezbędnymi modułami Ninject (ten na razie powyżej):

public partial class App : Application
{       
    protected override void OnStartup(StartupEventArgs e)
    {
        IocKernel.Initialize(new IocConfiguration());

        base.OnStartup(e);
    }
}

Użyłem statycznej klasy IocKernel do trzymania jądro IoC w całej aplikacji, dzięki czemu mogę łatwo uzyskać do niego dostęp w razie potrzeby:

public static class IocKernel
{
    private static StandardKernel _kernel;

    public static T Get<T>()
    {
        return _kernel.Get<T>();
    }

    public static void Initialize(params INinjectModule[] modules)
    {
        if (_kernel == null)
        {
            _kernel = new StandardKernel(modules);
        }
    }
}

To rozwiązanie wykorzystuje statyczny ServiceLocator (IocKernel), który jest powszechnie uważany za anty-wzorzec, ponieważ ukrywa zależności klasy. Jednak bardzo trudno jest uniknąć ręcznego wyszukiwania usług dla klas interfejsu użytkownika, ponieważ muszą one mieć konstruktor bez parametru i nie możesz kontrolować instancji, więc nie możesz wstrzyknąć maszyny wirtualnej. Przynajmniej w ten sposób umożliwia testowanie maszyny Wirtualnej w izolacji, gdzie znajduje się cała logika biznesowa.

Jeśli ktoś ma lepszy sposób, proszę się podzielić.

Edytuj: Lucky Likey dostarczył odpowiedź, aby pozbyć się statycznego lokalizatora usług, pozwalając Ninject tworzyć instancje klas interfejsu użytkownika. Szczegóły odpowiedzi można zobaczyć tutaj

 66
Author: sondergard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:38

W twoim pytaniu ustawiasz wartość właściwości DataContext widoku w XAML. Wymaga to, aby model widoku miał domyślny konstruktor. Jednak, jak już zauważyłeś, nie działa to dobrze w przypadku iniekcji zależności, gdzie chcesz wprowadzić zależności w konstruktorze.

Więc nie można ustawić właściwości DataContext w XAML. Zamiast tego masz inne alternatywy.

Jeśli aplikacja jest oparta na prostym hierarchicznym modelu widoku-można skonstruować całą hierarchia modelu widoku po uruchomieniu aplikacji (musisz usunąć właściwość StartupUri z pliku App.xaml):

public partial class App {

  protected override void OnStartup(StartupEventArgs e) {
    base.OnStartup(e);
    var container = CreateContainer();
    var viewModel = container.Resolve<RootViewModel>();
    var window = new MainWindow { DataContext = viewModel };
    window.Show();
  }

}

Jest to oparte na obiektowym wykresie modeli widoków zakorzenionych w RootViewModel, ale można wprowadzić kilka fabryk modeli widoków do nadrzędnych modeli widoków, pozwalając im tworzyć nowe modele widoku potomnego, aby Wykres obiektowy nie musiał być naprawiany. Mam nadzieję, że to również odpowiada na twoje pytanie Załóżmy, że potrzebuję przykładu SomeViewModel z mojego kodu cs, Jak mam to zrobić to?

class ParentViewModel {

  public ParentViewModel(ChildViewModelFactory childViewModelFactory) {
    _childViewModelFactory = childViewModelFactory;
  }

  public void AddChild() {
    Children.Add(_childViewModelFactory.Create());
  }

  ObservableCollection<ChildViewModel> Children { get; private set; }

 }

class ChildViewModelFactory {

  public ChildViewModelFactory(/* ChildViewModel dependencies */) {
    // Store dependencies.
  }

  public ChildViewModel Create() {
    return new ChildViewModel(/* Use stored dependencies */);
  }

}

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 widoku na model widoku. Możesz to zrobić widok pierwszy gdzie wybrać widok-model na podstawie widoku lub możesz to zrobić widok-model pierwszy gdzie Widok-model określa, którego widoku użyć. Framework MVVM zapewnia tę kluczową funkcjonalność w jakiś sposób, aby podłączyć kontener DI do tworzenia modeli widoków, ale możesz również zaimplementować go samodzielnie. Jestem tu trochę niejasny, ponieważ w zależności od twoich potrzeb ta funkcjonalność może stać się dość złożona. Jest to jedna z podstawowych funkcji, które otrzymujesz z frameworku MVVM, ale toczenie własnej w prostej aplikacji da ci dobre zrozumienie, co zapewniają frameworki MVVM pod hood.

Nie mogÄ ... c zadeklarowaÄ ‡ DataContext w XAML tracisz wsparcie czasu projektowania. Jeśli twój model widoku zawiera pewne dane, pojawi się w czasie projektowania, co może być bardzo przydatne. Na szczęście możesz używać atrybutów design-time również w WPF. Jednym ze sposobów jest dodanie następujących atrybutów do elementu <Window> lub <UserControl> w XAML:

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:MyViewModel, IsDesignTimeCreatable=True}"

Typ view-model powinien mieć dwa konstruktory, domyślny dla danych czasu projektowania i inny dla zależności iniekcja: {]}

class MyViewModel : INotifyPropertyChanged {

  public MyViewModel() {
    // Create some design-time data.
  }

  public MyViewModel(/* Dependencies */) {
    // Store dependencies.
  }

}

W ten sposób możesz użyć dependency injection i zachować dobre wsparcie w czasie projektowania.

 38
Author: Martin Liversage,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-08 19:47:58

To co tu zamieszczam jest poprawką do odpowiedzi sondergarda, bo to co powiem nie pasuje do komentarza:)

W rzeczywistości wprowadzam schludne rozwiązanie, które pozwala uniknąć potrzeby ServiceLocator i wrapper dla instancji StandardKernel, która w rozwiązaniu sondergard nazywa się IocContainer. Dlaczego? Jak wspomniano, są to anty-wzorce.

Udostępnienie StandardKernel wszędzie

Kluczem do magii Ninject jest StandardKernel-instancja, która jest potrzebne do użycia metody .Get<T>() -.

Alternatywnie do sondergardu IocContainer możesz utworzyć StandardKernel wewnątrz klasy App.

Po Prostu Usuń StartUpUri ze swojej aplikacji.xaml

<Application x:Class="Namespace.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
             ... 
</Application>

To jest kod aplikacji w aplikacji.xaml.cs

public partial class App
{
    private IKernel _iocKernel;

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        _iocKernel = new StandardKernel();
        _iocKernel.Load(new YourModule());

        Current.MainWindow = _iocKernel.Get<MainWindow>();
        Current.MainWindow.Show();
    }
}
Od teraz Ninject żyje i jest gotowy do walki:)]}

Wstrzykiwanie leku DataContext

Ponieważ Ninject żyje, możesz wykonywać wszelkiego rodzaju zastrzyki, np Property Setter Injection lub najczęściej jeden Wtrysk konstruktora .

Oto jak wstrzykujesz swój ViewModel do swoich Window ' s DataContext

public partial class MainWindow : Window
{
    public MainWindow(MainWindowViewModel vm)
    {
        DataContext = vm;
        InitializeComponent();
    }
}

Oczywiście możesz również wstrzyknąć IViewModel, jeśli zrobisz odpowiednie wiązania, ale to nie jest częścią tej odpowiedzi.

Bezpośredni dostęp do jądra

Jeśli trzeba wywołać metody bezpośrednio w jądrze (np. .Get<T>() - metoda), możesz pozwolić, aby jądro samo się wstrzyknęło.

    private void DoStuffWithKernel(IKernel kernel)
    {
        kernel.Get<Something>();
        kernel.Whatever();
    }

Jeśli potrzebujesz lokalnej instancji Jądra, możesz wstrzyknąć to jako własność.

    [Inject]
    public IKernel Kernel { private get; set; }

Mimo, że może to być całkiem przydatne, nie polecam ci tego robić. Zauważ tylko, że obiekty wstrzyknięte w ten sposób nie będą dostępne wewnątrz konstruktora, ponieważ zostaną wstrzyknięte później.

Zgodnie z tym linkiem powinieneś użyć factory-Extension zamiast wstrzykiwać IKernel (kontener DI).

Zalecanym podejściem do stosowania kontenera DI w systemie oprogramowania jest to, że korzeń kompozycji aplikacja to pojedyncze miejsce, w którym bezpośrednio dotykany jest pojemnik.

Jak Ninject.Rozszerzenia.Fabryka ma być używana może być również czerwona tutaj .

 16
Author: LuckyLikey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-26 12:51:10

Wybieram podejście "widok pierwszy", gdzie przekazuję model widoku do konstruktora widoku (w kodzie-za), który zostaje przypisany do kontekstu danych, np.

public class SomeView
{
    public SomeView(SomeViewModel viewModel)
    {
        InitializeComponent();

        DataContext = viewModel;
    }
}

Zastępuje to podejście oparte na XAML.

Używam frameworku Prism do obsługi nawigacji-gdy jakiś kod żąda wyświetlenia określonego widoku (przez" nawigację " do niego), Prism rozwiąże ten widok (wewnętrznie, używając frameworku di aplikacji); framework DI z kolei rozwiąże wszelkie zależności, które Widok ma (model widoku w moim przykładzie), a następnie rozwiązuje jego zależności, i tak dalej.

Wybór frameworku DI jest praktycznie nieistotny, ponieważ wszystkie one zasadniczo robią to samo, tzn. rejestrujesz interfejs (lub typ) wraz z konkretnym typem, który chcesz, aby Framework utworzył instancję, gdy znajdzie zależność od tego interfejsu. Dla przypomnienia używam Castle Windsor.

Nawigacja Prism wymaga trochę przyzwyczajenia, ale jest całkiem dobra, gdy już zaczniesz myśleć pozwala na komponowanie aplikacji przy użyciu różnych widoków. Na przykład możesz utworzyć "region" Prism w głównym oknie, a następnie za pomocą nawigacji Prism przełączysz się z jednego widoku do drugiego w tym regionie, np. gdy użytkownik wybiera pozycje menu lub cokolwiek innego.

Alternatywnie spójrz na jedną z frameworków MVVM, taką jak MVVM Light. Nie mam doświadczenia z tymi, więc nie mogę skomentować tego, co lubią używać.

 11
Author: Andrew Stephens,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-19 09:47:45

Zainstaluj MVVM Light.

Częścią instalacji jest utworzenie lokalizatora modelu widoku. Jest to klasa, która eksponuje Twoje viewmodels jako właściwości. Getter tych właściwości mogą być następnie zwracane instancje z silnika IOC. Na szczęście, MVVM light zawiera również prosty framework, ale możesz drukować w innych, jeśli chcesz.

W simple IOC rejestrujesz implementację względem typu...

SimpleIOC.Default.Register<MyViewModel>(()=> new MyViewModel(new ServiceProvider()), true);

W tym przykładzie model widoku jest tworzony i przekazywany obiekt usługodawcy według jego konstruktora.

Następnie tworzy się właściwość, która zwraca instancję z IOC.

public MyViewModel
{
    get { return SimpleIOC.Default.GetInstance<MyViewModel>; }
}

Sprytne jest to, że lokalizator modelu widoku jest następnie tworzony w aplikacji.xaml lub równoważny jako źródło danych.

<local:ViewModelLocator x:key="Vml" />

Możesz teraz powiązać z jego właściwością "MyViewModel", aby uzyskać swój viewmodel z wprowadzoną usługą.

Mam nadzieję, że to pomoże. Przepraszamy za wszelkie nieścisłości kodu, zakodowane z pamięci na iPadzie.
 11
Author: kidshaw,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-22 13:26:08

Użyj Managed Extensibility Framework.

[Export(typeof(IViewModel)]
public class SomeViewModel : IViewModel
{
    private IStorage _storage;

    [ImportingConstructor]
    public SomeViewModel(IStorage storage){
        _storage = storage;
    }

    public bool ProperlyInitialized { get { return _storage != null; } }
}

[Export(typeof(IStorage)]
public class Storage : IStorage
{
    public bool SaveFile(string content){
        // Saves the file using StreamWriter
    }
}

//Somewhere in your application bootstrapping...
public GetViewModel() {
     //Search all assemblies in the same directory where our dll/exe is
     string currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
     var catalog = new DirectoryCatalog(currentPath);
     var container = new CompositionContainer(catalog);
     var viewModel = container.GetExport<IViewModel>();
     //Assert that MEF did as advertised
     Debug.Assert(viewModel is SomViewModel); 
     Debug.Assert(viewModel.ProperlyInitialized);
}

Ogólnie rzecz biorąc, to co byś zrobił to mieć statyczną klasę i użyć wzorca fabrycznego, aby zapewnić Ci globalny kontener (buforowany, natch).

Jeśli chodzi o sposób wstrzykiwania modeli widoków, wstrzykuje się je w ten sam sposób, w jaki wstrzykuje się Wszystko inne. Utwórz konstruktor importujący (lub umieść instrukcję import na właściwości / polu) w kodzie pliku XAML i powiedz mu, aby zaimportował model widoku. Następnie zwiąż swoje Window ' s DataContext do tej nieruchomości. Obiekty root, które sam wyciągasz z kontenera, to zazwyczaj złożone obiekty Window. Wystarczy dodać interfejsy do klas okien i wyeksportować je, a następnie pobrać z katalogu jak powyżej (w aplikacji.xaml.cs... to plik Bootstrap WPF).

 3
Author: Clever Neologism,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-25 21:35:56

Proponuję skorzystać z podejścia ViewModel - First https://github.com/Caliburn-Micro/Caliburn.Micro

Zobacz: https://caliburnmicro.codeplex.com/wikipage?title=All%20About%20Conventions

Użyj Castle Windsor jako kontenera IOC.

Wszystko O Konwencjach

Jedna z głównych cech Caliburn.Micro przejawia się w swojej zdolności do usuwania potrzeby kodu płyty kotłowej poprzez działanie na szeregu konwencji. Jedni kochają konwencje, inni nienawidzą oni. Dlatego konwencje CM są w pełni konfigurowalne, a nawet można je 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ć, czym są te konwencje i jak działają. To jest temat tego artykułu. Zobacz Rozdzielczość (ViewModel-First)

Podstawy

Pierwsza konwencja, z którą możesz się spotkać podczas korzystania z CM, jest związana z rozdzielczością widoku. Ta konwencja wpływa na wszystkie obszary ViewModel-pierwsze obszary twojego podanie. W ViewModel-po pierwsze, mamy istniejący ViewModel, który musimy renderować na ekranie. W tym celu CM używa prostego wzorca nazewnictwa, aby znaleźć kontrolkę UserControl1, która powinna być powiązana z ViewModel i display. Co to za wzór? Rzućmy okiem na ViewLocator.LocateForModelType, aby dowiedzieć się:

public static Func<Type, DependencyObject, object, UIElement> LocateForModelType = (modelType, displayLocation, context) =>{
    var viewTypeName = modelType.FullName.Replace("Model", string.Empty);
    if(context != null)
    {
        viewTypeName = viewTypeName.Remove(viewTypeName.Length - 4, 4);
        viewTypeName = viewTypeName + "." + context;
    }

    var viewType = (from assmebly in AssemblySource.Instance
                    from type in assmebly.GetExportedTypes()
                    where type.FullName == viewTypeName
                    select type).FirstOrDefault();

    return viewType == null
        ? new TextBlock { Text = string.Format("{0} not found.", viewTypeName) }
        : GetOrCreateViewType(viewType);
};

Zignorujmy zmienną "context" na początku. Aby uzyskać widok, Zakładamy, że używasz tekstu "ViewModel" w nazewnictwie maszyn wirtualnych, więc po prostu zmień to na" widok "wszędzie tam, gdzie go znajdziemy, usuwając słowo "Model". Powoduje to zmianę zarówno nazw typów, jak i przestrzeni nazw. Więc ViewModels.CustomerViewModel stałby się widokami.CustomerView. Lub jeśli organizujesz swoją aplikację według funkcji: CustomerManagement.CustomerViewModel staje się CustomerManagement.CustomerView. Mam nadzieję, że to całkiem proste. Gdy już mamy nazwę, szukamy typów o tej nazwie. Wyszukujemy każdy zespół wystawiony na działanie CM jako możliwość wyszukiwania poprzez AssemblySource.Przykład.2 jeśli znajdziemy Typ, tworzymy instancję (lub pobieramy ją z kontenera IoC, jeśli jest zarejestrowana) i zwracamy ją do wywołującego. Jeśli nie znajdziemy typu, generujemy widok z odpowiednim komunikatem "not found".

Wróćmy do tej wartości "kontekstowej". W ten sposób CM obsługuje wiele widoków na tym samym modelu widoku. Jeśli podano kontekst (zazwyczaj łańcuch lub enum), wykonujemy dalszą transformację nazwy w oparciu o tę wartość. To transformacja zakłada, że masz folder (przestrzeń nazw) dla różnych widoków, usuwając słowo "widok" z końca i dodając zamiast tego kontekst. Tak więc, biorąc pod uwagę kontekst "Master" naszych modeli widzenia.CustomerViewModel stałby się widokami.Klient.Mistrzu.

 0
Author: Nahum,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-26 07:54:29

Usuń Uri startowe z aplikacji.xaml.

App.xaml.cs

public partial class App
{
    protected override void OnStartup(StartupEventArgs e)
    {
        IoC.Configure(true);

        StartupUri = new Uri("Views/MainWindowView.xaml", UriKind.Relative);

        base.OnStartup(e);
    }
}

Teraz możesz użyć swojej klasy IoC do konstruowania instancji.

MainWindowView.xaml.cs

public partial class MainWindowView
{
    public MainWindowView()
    {
        var mainWindowViewModel = IoC.GetInstance<IMainWindowViewModel>();

        //Do other configuration            

        DataContext = mainWindowViewModel;

        InitializeComponent();
    }

}
 0
Author: C Bauer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-27 13:47:45