簡體   English   中英

如何處理 WPF/MVVM 應用程序中的依賴注入

[英]How to handle dependency injection in a WPF/MVVM application

我正在啟動一個新的桌面應用程序,我想使用 MVVM 和 WPF 構建它。

我也打算使用 TDD。

問題是我不知道我應該如何使用 IoC 容器將我的依賴項注入到我的生產代碼中。

假設我有以下 class 和接口:

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

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

然后我有另一個 class 將IStorage作為依賴項,還假設這個 class 是一個 ViewModel 或一個業務 class...

public class SomeViewModel
{
    private IStorage _storage;

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

有了這個,我可以輕松編寫單元測試以確保它們正常工作,使用模擬等。

問題是在實際應用程序中使用它時。 我知道我必須有一個 IoC 容器來鏈接IStorage接口的默認實現,但我該怎么做呢?

例如,如果我有以下 xaml 會怎樣:

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

在這種情況下,我如何正確地“告訴”WPF 注入依賴項?

另外,假設我需要 C# 代碼中的SomeViewModel實例,我應該怎么做?

我覺得我完全迷失了這一點,我會很感激任何關於如何處理它的最佳方法的例子或指導。

我熟悉 StructureMap,但我不是專家。 另外,如果有更好/更簡單/開箱即用的框架,請告訴我。

我一直在使用 Ninject,發現使用它很愉快。 一切都在代碼中設置,語法相當簡單,並且有很好的文檔(以及很多關於 SO 的答案)。

所以基本上它是這樣的:

創建視圖模型,並將IStorage接口作為構造函數參數:

class UserControlViewModel
{
    public UserControlViewModel(IStorage storage)
    {

    }
}

為視圖模型創建一個帶有 get 屬性的ViewModelLocator ,它從 Ninject 加載視圖模型:

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

在 App.xaml 中使ViewModelLocator成為應用程序范圍的資源:

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

UserControlDataContext綁定到 ViewModelLocator 中的相應屬性。

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

創建一個繼承 NinjectModule 的類,它將設置必要的綁定( IStorage和視圖模型):

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
    }
}

在應用程序啟動時使用必要的 Ninject 模塊(目前上面的模塊)初始化 IoC 內核:

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

        base.OnStartup(e);
    }
}

我使用了一個靜態IocKernel類來保存 IoC 內核的應用程序范圍的實例,因此我可以在需要時輕松訪問它:

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);
        }
    }
}

這個解決方案確實使用了一個靜態的ServiceLocatorIocKernel ),它通常被認為是一種反模式,因為它隱藏了類的依賴關系。 然而,很難避免對 UI 類進行某種手動服務查找,因為它們必須有一個無參數的構造函數,而且您無論如何都無法控制實例化,因此您無法注入 VM。 至少這種方式允許您單獨測試 VM,這是所有業務邏輯所在的位置。

如果有人有更好的方法,請分享。

編輯:Lucky Likey 通過讓 Ninject 實例化 UI 類,提供了擺脫靜態服務定位器的答案。 答案的詳細信息可以在這里看到

在您的問題中,您在 XAML 中設置視圖的DataContext屬性的值。 這要求您的視圖模型具有默認構造函數。 但是,正如您所指出的,這不適用於您希望在構造函數中注入依賴項的依賴項注入。

所以你不能在 XAML 中設置DataContext屬性 相反,您有其他選擇。

如果您的應用程序基於簡單的分層視圖模型,則可以在應用程序啟動時構建整個視圖模型層次結構(您必須從App.xaml文件中刪除StartupUri屬性):

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();
  }

}

這是基於以RootViewModel根的視圖模型的對象圖,但您可以將一些視圖模型工廠注入父視圖模型,允許它們創建新的子視圖模型,因此不必修復對象圖。 這也有希望回答你的問題,假設我需要一個來自我的cs代碼的SomeViewModel實例,我應該怎么做?

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 */);
  }

}

如果您的應用程序本質上更具動態性並且可能基於導航,則您將不得不掛鈎執行導航的代碼。 每次導航到新視圖時,您都需要創建一個視圖模型(來自 DI 容器)、視圖本身並將視圖的DataContext設置為視圖模型。 您可以先執行此視圖,然后根據視圖選擇視圖模型,也可以先執行視圖模型,其中視圖模型確定要使用的視圖。 MVVM 框架通過某種方式提供了這個關鍵功能,讓您可以將 DI 容器連接到視圖模型的創建中,但您也可以自己實現它。 我在這里有點含糊,因為根據您的需要,此功能可能會變得非常復雜。 這是您從 MVVM 框架獲得的核心功能之一,但在一個簡單的應用程序中滾動您自己的功能將使您很好地了解 MVVM 框架在幕后提供的功能。

由於無法在 XAML 中聲明DataContext ,您將失去一些設計時支持。 如果您的視圖模型包含一些數據,它會在設計時出現,這非常有用。 幸運的是,您也可以在 WPF 中使用 設計時屬性 一種方法是將以下屬性添加到 XAML 中的<Window>元素或<UserControl>

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}"

視圖模型類型應該有兩個構造函數,默認用於設計時數據,另一個用於依賴注入:

class MyViewModel : INotifyPropertyChanged {

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

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

}

通過這樣做,您可以使用依賴注入並保留良好的設計時支持。

我在這里發布的是對sondergard 的回答的改進,因為我要說的內容不適合評論:)

事實上,我正在引入一個簡潔的解決方案,它避免了ServiceLocatorStandardKernel -Instance 的包裝器的需要,在 Sondergard 的解決方案中稱為IocContainer 為什么? 如前所述,這些是反模式。

使StandardKernel隨處可用

Ninject 魔法的關鍵是使用.Get<T>() -Method 所需的StandardKernel -Instance。

除了 Sondergard 的IocContainer您還可以在App IocContainer中創建StandardKernel

只需從 App.xaml 中刪除 StartUpUri

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

這是 App.xaml.cs 中應用程序的 CodeBehind

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();
    }
}

從現在開始,Ninject 還活着並准備好戰斗 :)

注入你的DataContext

由於 Ninject 還活着,您可以執行各種注入,例如Property Setter Injection或最常見的一種Constructor Injection

這就是將 ViewModel 注入WindowDataContext

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

當然,如果您進行了正確的綁定,您也可以注入IViewModel ,但這不是本答案的一部分。

直接訪問內核

如果您需要直接調用內核上的方法(例如.Get<T>() -Method),您可以讓內核自行注入。

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

如果您需要內核的本地實例,您可以將其作為屬性注入。

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

盡管這可能非常有用,但我不建議您這樣做。 請注意,以這種方式注入的對象在構造函數中將不可用,因為它是稍后注入的。

根據此鏈接,您應該使用 factory-Extension 而不是注入IKernel (DI 容器)。

在軟件系統中使用 DI 容器的推薦方法是,應用程序的組合根是直接接觸容器的單一位置。

如何使用 Ninject.Extensions.Factory 在這里也可以是紅色的。

我采用“視圖優先”方法,將視圖模型傳遞給視圖的構造函數(在其代碼隱藏中),該構造函數被分配給數據上下文,例如

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

        DataContext = viewModel;
    }
}

這將取代基於 XAML 的方法。

我使用 Prism 框架來處理導航——當某些代碼請求顯示特定視圖時(通過“導航”到它),Prism 將解析該視圖(在內部,使用應用程序的 DI 框架); DI 框架將依次解析視圖具有的任何依賴項(在我的示例中為視圖模型),然后解析依賴項,依此類推。

DI 框架的選擇幾乎無關緊要,因為它們基本上都做相同的事情,即您注冊一個接口(或類型)以及您希望框架在發現對該接口的依賴時實例化的具體類型。 為了記錄,我使用了溫莎城堡。

Prism 導航需要一些時間來適應,但是一旦您了解它就非常好,允許您使用不同的視圖組合您的應用程序。 例如,您可以在主窗口上創建一個 Prism“區域”,然后使用 Prism 導航,您可以在該區域內從一個視圖切換到另一個視圖,例如,當用戶選擇菜單項或其他任何內容時。

或者,看看其中一個 MVVM 框架,例如 MVVM Light。 我沒有這些經驗,所以不能評論他們喜歡使用什么。

安裝 MVVM 燈。

安裝的一部分是創建一個視圖模型定位器。 這是一個將您的視圖模型公開為屬性的類。 然后可以從 IOC 引擎返回這些屬性的 getter 實例。 幸運的是,MVVM light 還包括 SimpleIOC 框架,但您可以根據需要連接其他框架。

使用簡單的 IOC,您可以針對類型注冊一個實現......

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

在這個例子中,你的視圖模型被創建並根據它的構造函數傳遞了一個服務提供者對象。

然后創建一個屬性,該屬性從 IOC 返回一個實例。

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

聰明的部分是視圖模型定位器然后在 app.xaml 或等效的數據源中創建。

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

您現在可以綁定到它的 'MyViewModel' 屬性以獲取具有注入服務的視圖模型。

希望有幫助。 對於在 iPad 上從內存中編碼的任何代碼不准確之處,我們深表歉意。

佳能 DryIoc 案例

回答一個舊帖子,但用DryIoc做這DryIoc並做我認為很好地使用 DI 和接口的事情(最少使用具體類)。

  1. WPF 應用程序的起點是App.xaml ,在那里我們告訴要使用的初始視圖是什么; 我們使用后面的代碼而不是默認的 xaml 來做到這一點:
  2. 刪除 App.xaml 中的StartupUri="MainWindow.xaml"
  3. 在代碼隱藏 (App.xaml.cs) 中添加此override OnStartup

     protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); DryContainer.Resolve<MainWindow>().Show(); }

這就是啟動點; 這也是唯一應該調用resolve地方。

  1. 配置根(根據 Mark Seeman 的書 .NET 中的依賴注入;應該提到具體類的唯一地方)將在相同的代碼隱藏中,在構造函數中:

     public Container DryContainer { get; private set; } public App() { DryContainer = new Container(rules => rules.WithoutThrowOnRegisteringDisposableTransient()); DryContainer.Register<IDatabaseManager, DatabaseManager>(); DryContainer.Register<IJConfigReader, JConfigReader>(); DryContainer.Register<IMainWindowViewModel, MainWindowViewModel>( Made.Of(() => new MainWindowViewModel(Arg.Of<IDatabaseManager>(), Arg.Of<IJConfigReader>()))); DryContainer.Register<MainWindow>(); }

備注和更多細節

  • 我只在視圖MainWindow使用了具體類;
  • 我必須為 ViewModel 指定要使用的構造函數(我們需要使用 DryIoc 來實現),因為 XAML 設計器需要存在默認構造函數,而具有注入功能的構造函數是實際用於應用程序的構造函數。

帶有 DI 的 ViewModel 構造函數:

public MainWindowViewModel(IDatabaseManager dbmgr, IJConfigReader jconfigReader)
{
    _dbMgr = dbmgr;
    _jconfigReader = jconfigReader;
}

用於設計的 ViewModel 默認構造函數:

public MainWindowViewModel()
{
}

視圖的代碼隱藏:

public partial class MainWindow
{
    public MainWindow(IMainWindowViewModel vm)
    {
        InitializeComponent();
        ViewModel = vm;
    }

    public IViewModel ViewModel
    {
        get { return (IViewModel)DataContext; }
        set { DataContext = value; }
    }
}

以及在視圖 (MainWindow.xaml) 中使用 ViewModel 獲取設計實例所需的內容:

d:DataContext="{d:DesignInstance local:MainWindowViewModel, IsDesignTimeCreatable=True}"

結論

因此,我們使用 DryIoc 容器和 DI 獲得了一個非常干凈和最小的 WPF 應用程序實現,同時保持視圖和視圖模型的設計實例成為可能。

使用托管可擴展性框架

[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);
}

通常,您要做的是擁有一個靜態類並使用工廠模式為您提供一個全局容器(緩存、natch)。

至於如何注入視圖模型,您可以像注入其他所有內容一樣注入它們。 在 XAML 文件的代碼隱藏中創建一個導入構造函數(或在屬性/字段上放置一個導入語句),並告訴它導入視圖模型。 然后將您的WindowDataContext綁定到該屬性。 您自己實際從容器中拉出的根對象通常是組合的Window對象。 只需將接口添加到窗口類,然后導出它們,然后從目錄中抓取,如上(在 App.xaml.cs 中...這是 WPF 引導文件)。

我建議使用 ViewModel - 第一種方法https://github.com/Caliburn-Micro/Caliburn.Micro

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

使用Castle Windsor作為 IOC 容器。

所有關於約定

Caliburn.Micro 的主要特性之一體現在它能夠通過根據一系列約定來消除對樣板代碼的需求。 有些人喜歡公約,有些人討厭它們。 這就是為什么 CM 的約定是完全可定制的,如果不需要,甚至可以完全關閉。 如果您打算使用約定,並且由於默認情況下它們處於啟用狀態,那么最好了解這些約定是什么以及它們如何工作。 這就是本文的主題。 視圖分辨率(ViewModel-First)

基本

使用 CM 時您可能會遇到的第一個約定與視圖分辨率有關。 此約定會影響應用程序的任何 ViewModel-First 區域。 在 ViewModel-First 中,我們有一個需要渲染到屏幕的現有 ViewModel。 為此,CM 使用一個簡單的命名模式來查找它應該綁定到 ViewModel 並顯示的 UserControl1。 那么,那是什么模式呢? 我們來看看 ViewLocator.LocateForModelType 就知道了:

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);
};

首先讓我們忽略“上下文”變量。 為了導出視圖,我們假設您在 VM 的命名中使用文本“ViewModel”,因此我們只需通過刪除“Model”一詞將其更改為“View”。 這具有更改類型名稱和命名空間的效果。 所以 ViewModels.CustomerViewModel 會變成 Views.CustomerView。 或者,如果您按功能組織應用程序:CustomerManagement.CustomerViewModel 將變為 CustomerManagement.CustomerView。 希望這是非常直接的。 獲得名稱后,我們將搜索具有該名稱的類型。 我們通過 AssemblySource.Instance.2 搜索您向 CM 公開的任何程序集,如果我們找到該類型,我們將創建一個實例(如果已注冊,則從 IoC 容器中獲取一個實例)並將其返回給調用者。 如果我們沒有找到類型,我們會生成一個帶有適當“未找到”消息的視圖。

現在,回到那個“上下文”值。 這就是 CM 在同一個 ViewModel 上支持多個視圖的方式。 如果提供了上下文(通常是字符串或枚舉),我們會根據該值對名稱進行進一步的轉換。 通過從末尾刪除“視圖”一詞並附加上下文,此轉換有效地假設您有一個用於不同視圖的文件夾(命名空間)。 因此,給定“Master”的上下文,我們的 ViewModels.CustomerViewModel 將成為 Views.Customer.Master。

從 app.xaml 中刪除啟動 uri。

應用程序.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);
    }
}

現在您可以使用您的 IoC 類來構造實例。

主窗口視圖.xaml.cs

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

        //Do other configuration            

        DataContext = mainWindowViewModel;

        InitializeComponent();
    }

}

另一個簡單的解決方案是創建一個 murkup 擴展,根據其類型解析您的視圖 model:

public class DISource : MarkupExtension {
    public static Func<Type, object, string, object> Resolver { get; set; }

    public Type Type { get; set; }
    public object Key { get; set; }
    public string Name { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider) => Resolver?.Invoke(Type, Key, Name);
}

您可以通過以下方式將此擴展調整為任何 DI 容器:

protected override void OnStartup(StartupEventArgs e) {
    base.OnStartup(e);
    DISource.Resolver = Resolve;
}
object Resolve(Type type, object key, string name) {
    if(type == null)
        return null;
    if(key != null)
        return Container.ResolveKeyed(key, type);
    if(name != null)
        return Container.ResolveNamed(name, type);
    return Container.Resolve(type);
}

在 XAML 中使用它就這么簡單:

DataContext="{local:DISource Type=viewModels:MainViewModel}"

這樣,您將能夠輕松地將 DataContext 分配給您的視圖,並使用您的 DI 容器自動將所有必需的參數直接注入您的視圖 model。 使用這種技術,您不必將 DI 容器或其他參數傳遞給 View 構造函數。

DISource 不依賴於容器類型,因此您可以將它與任何依賴注入框架一起使用。 將 DISource.Resolver 屬性設置為知道如何使用 DI 容器的方法就足夠了。

我在Dependency Injection in a WPF MVVM Application中更詳細地描述了這項技術

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM