วิธีจัดการการพึ่งพาการฉีดในแอปพลิเคชัน WPF / MVVM


105

ฉันกำลังเริ่มแอปพลิเคชันเดสก์ท็อปใหม่และต้องการสร้างโดยใช้ MVVM และ WPF

ฉันตั้งใจจะใช้ TDD ด้วย

ปัญหาคือฉันไม่รู้ว่าฉันควรใช้คอนเทนเนอร์ IoC เพื่อฉีดการอ้างอิงกับรหัสการผลิตของฉันอย่างไร

สมมติว่าฉันมีคลาสและอินเทอร์เฟซต่อไปนี้:

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

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

แล้วฉันก็มีคลาสอื่นที่มีIStorageการพึ่งพาสมมติว่าคลาสนี้เป็น ViewModel หรือชั้นธุรกิจ ...

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 ให้ฉีดการอ้างอิงในกรณีนั้นอย่างถูกต้องได้อย่างไร?

นอกจากนี้สมมติว่าฉันต้องการอินสแตนซ์SomeViewModelจากรหัส C # ของฉันฉันควรทำอย่างไร

ฉันรู้สึกว่าฉันหลงทางกับเรื่องนี้อย่างสิ้นเชิงฉันขอขอบคุณตัวอย่างหรือคำแนะนำว่าวิธีที่ดีที่สุดในการจัดการกับมันอย่างไร

ฉันคุ้นเคยกับ StructureMap แต่ฉันไม่ใช่ผู้เชี่ยวชาญ นอกจากนี้หากมีเฟรมเวิร์กที่ดีกว่า / ง่ายกว่า / นอกกรอบโปรดแจ้งให้เราทราบ


ด้วย. net core 3.0 ในการแสดงตัวอย่างคุณสามารถทำได้กับแพ็คเกจ Microsoft nuget
Bailey Miller

คำตอบ:


88

ฉันใช้ Ninject และพบว่ามันมีความสุขที่ได้ร่วมงานกับ ทุกอย่างถูกตั้งค่าในโค้ดไวยากรณ์ค่อนข้างตรงไปตรงมาและมีเอกสารที่ดี (และคำตอบมากมายเกี่ยวกับ SO)

โดยพื้นฐานแล้วมันจะเป็นดังนี้:

สร้างโมเดลมุมมองและใช้IStorageอินเทอร์เฟซเป็นพารามิเตอร์ตัวสร้าง:

class UserControlViewModel
{
    public UserControlViewModel(IStorage storage)
    {

    }
}

สร้างViewModelLocatorด้วยคุณสมบัติ get สำหรับโมเดลมุมมองซึ่งโหลดโมเดลมุมมองจาก Ninject:

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

สร้างViewModelLocatorทรัพยากรแอปพลิเคชันให้กว้างใน App.xaml:

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

ผูกDataContextของเข้าUserControlกับคุณสมบัติที่เกี่ยวข้องใน ViewModelLocator

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

สร้างคลาสที่สืบทอด NinjectModule ซึ่งจะตั้งค่าการผูกที่จำเป็น ( IStorageและ 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
    }
}

เริ่มต้นเคอร์เนล IoC เมื่อเริ่มต้นแอ็พพลิเคชันด้วยโมดูล Ninject ที่จำเป็น (อันที่อยู่ด้านบนสำหรับตอนนี้):

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

โซลูชันนี้ใช้ประโยชน์จาก static ServiceLocator(the IocKernel) ซึ่งโดยทั่วไปถือว่าเป็น anti-pattern เนื่องจากซ่อนการอ้างอิงของคลาส อย่างไรก็ตามเป็นการยากมากที่จะหลีกเลี่ยงการค้นหาบริการด้วยตนเองสำหรับคลาส UI เนื่องจากต้องมีตัวสร้างแบบไม่มีพารามิเตอร์และคุณไม่สามารถควบคุมอินสแตนซ์ได้ดังนั้นคุณจึงไม่สามารถฉีด VM ได้ อย่างน้อยวิธีนี้จะช่วยให้คุณสามารถทดสอบ VM แบบแยกซึ่งเป็นที่ที่ตรรกะทางธุรกิจทั้งหมดอยู่

หากใครมีวิธีที่ดีกว่านี้โปรดแบ่งปัน

แก้ไข: Lucky Likey ให้คำตอบในการกำจัดตัวระบุตำแหน่งบริการแบบคงที่โดยให้ Ninject สร้างอินสแตนซ์คลาส UI รายละเอียดของคำตอบสามารถดูได้ที่นี่


13
ฉันยังใหม่กับการฉีดแบบพึ่งพา แต่ในหัวใจของโซลูชันของคุณคือการรวมรูปแบบการต่อต้าน Service Locator กับ Ninject เนื่องจากคุณใช้ ViewModel Locator แบบคงที่ อาจมีคนโต้แย้งว่าการฉีดเสร็จสิ้นในไฟล์ Xaml ซึ่งมีโอกาสน้อยที่จะทดสอบ ฉันไม่มีวิธีแก้ปัญหาที่ดีกว่านี้และอาจจะใช้ของคุณ - แต่ฉันคิดว่าการพูดถึงสิ่งนี้ในคำตอบจะเป็นประโยชน์เช่นกัน
user3141326

ผู้ชายวิธีการแก้ปัญหาของคุณเป็นที่ดีเพียงแค่มีเพียงหนึ่ง "ปัญหา" DataContext="{Binding [...]}"ที่มีสายต่อไปนี้: สิ่งนี้ทำให้ VS-Designer เรียกใช้ Program-Code ทั้งหมดใน Constructor ของ ViewModel ในกรณีของฉัน Window กำลังดำเนินการและบล็อกการโต้ตอบใด ๆ กับ VS บางทีเราควรปรับเปลี่ยน ViewModelLocator ไม่ให้ค้นหา ViewModels "จริง" ใน Design-Time - อีกวิธีหนึ่งคือการ "ปิดการใช้งานรหัสโครงการ" ซึ่งจะป้องกันไม่ให้แสดงทุกอย่างอื่น ๆ บางทีคุณอาจพบวิธีแก้ปัญหานี้เรียบร้อยแล้ว ในกรณีนี้ฉันขอให้คุณแสดง
LuckyLikey

@LuckyLikey คุณสามารถลองใช้ d: DataContext = "{d: DesignInstance vm: UserControlViewModel, IsDesignTimeCreatable = True}" แต่ฉันไม่แน่ใจว่ามันสร้างความแตกต่าง แต่ทำไม / อย่างไรตัวสร้าง VM จึงเปิดหน้าต่างโมดอล? และหน้าต่างแบบไหน?
sondergard

@son อันที่จริงฉันไม่รู้ว่าทำไมและอย่างไร แต่เมื่อฉันเปิด Window Designer จาก Solution Explorer เนื่องจากแท็บใหม่กำลังเปิดอยู่หน้าต่างจะแสดงโดยนักออกแบบและหน้าต่างเดียวกันจะปรากฏขึ้นราวกับว่ากำลังแก้ไขข้อบกพร่องโมดอล โฮสต์ในกระบวนการใหม่ภายนอก VS "Micorosoft Visual Studio XAML Designer" หากกระบวนการปิดตัวลง VS-Designer ก็ล้มเหลวด้วยข้อยกเว้นที่กล่าวถึงก่อนหน้า ฉันจะลองวิธีแก้ปัญหาของคุณ ฉันจะแจ้งให้คุณทราบเมื่อฉันตรวจพบข้อมูลใหม่ :)
LuckyLikey

1
@sondergard ฉันได้โพสต์การปรับปรุงคำตอบของคุณโดยหลีกเลี่ยง ServiceLocator Anti-Pattern อย่าลังเลที่จะตรวจสอบ
LuckyLikey

53

ในคำถามของคุณคุณตั้งค่าDataContextคุณสมบัติของมุมมองใน XAML สิ่งนี้ต้องการให้ view-model ของคุณมีตัวสร้างเริ่มต้น อย่างไรก็ตามตามที่คุณได้ระบุไว้สิ่งนี้ใช้ไม่ได้ผลกับการฉีดแบบพึ่งพาที่คุณต้องการฉีดการพึ่งพาในตัวสร้าง

ดังนั้นคุณจะไม่สามารถตั้งค่าDataContextคุณสมบัติใน XAML คุณมีทางเลือกอื่นแทน

หากแอปพลิเคชันของคุณใช้โมเดลมุมมองแบบลำดับชั้นอย่างง่ายคุณสามารถสร้างลำดับชั้นมุมมองโมเดลทั้งหมดเมื่อแอปพลิเคชันเริ่มทำงาน (คุณจะต้องลบStartupUriคุณสมบัติออกจาก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();
  }

}

สิ่งนี้เป็นไปตามกราฟออบเจ็กต์ของโมเดลมุมมองที่รูทRootViewModelแต่คุณสามารถฉีดโรงงานโมเดลมุมมองบางส่วนลงในโมเดลมุมมองหลักเพื่อให้พวกเขาสร้างโมเดลมุมมองลูกใหม่ได้ดังนั้นจึงไม่ต้องแก้ไขกราฟวัตถุ หวังว่าจะตอบคำถามของคุณได้เช่นกันสมมติว่าฉันต้องการตัวอย่างSomeViewModelจากcsรหัสของฉันฉันควรทำอย่างไร

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 มีให้อะไรบ้าง

การไม่สามารถประกาศDataContextใน XAML ได้คุณจะสูญเสียการสนับสนุนเวลาออกแบบบางส่วน หากโมเดลมุมมองของคุณมีข้อมูลบางส่วนข้อมูลจะปรากฏในช่วงเวลาออกแบบซึ่งมีประโยชน์มาก โชคดีที่คุณสามารถใช้แอตทริบิวต์เวลาออกแบบได้เช่นกันใน WPF วิธีหนึ่งที่ทำได้คือเพิ่มแอตทริบิวต์ต่อไปนี้ให้กับ<Window>องค์ประกอบหรือ<UserControl>ใน 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}"

ประเภทมุมมองแบบจำลองควรมีตัวสร้างสองตัวค่าเริ่มต้นสำหรับข้อมูลเวลาออกแบบและอีกตัวสำหรับการฉีดแบบพึ่งพา:

class MyViewModel : INotifyPropertyChanged {

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

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

}

ด้วยการทำเช่นนี้คุณสามารถใช้การฉีดแบบพึ่งพาและรักษาการสนับสนุนเวลาออกแบบได้ดี


13
นี่คือสิ่งที่ฉันกำลังมองหา มันทำให้ฉันหงุดหงิดกี่ครั้งที่อ่านคำตอบที่บอกว่า "ใช้กรอบงาน [ yadde-ya ]" นั่นคือทั้งหมดที่ดีและดี แต่ฉันต้องการทราบวิธีการม้วนตัวเองอย่างชัดเจนก่อนแล้วฉันจะได้รู้ว่าจริงๆแล้วเฟรมเวิร์กประเภทใดที่อาจใช้สำหรับฉัน ขอบคุณที่สะกดให้ชัดเจน
kmote

28

สิ่งที่ฉันโพสต์ต่อไปนี้เป็นการปรับปรุงคำตอบของ sondergard เพราะสิ่งที่ฉันจะบอกไม่เข้ากับความคิดเห็น :)

ในความเป็นจริงผมแนะนำวิธีการแก้ปัญหาเรียบร้อยซึ่งหลีกเลี่ยงความจำเป็นของการServiceLocatorและเสื้อคลุมสำหรับที่StandardKernel-Instance IocContainerซึ่งSøndergårdโซลูชั่นที่เรียกว่า ทำไม? ดังที่กล่าวมานั้นเป็นรูปแบบการต่อต้าน

ทำให้StandardKernelสามารถใช้ได้ทุกที่

กุญแจสู่เวทมนตร์ของ Ninject คือStandardKernel-Instance ซึ่งจำเป็นในการใช้.Get<T>()-Method

อีกทางเลือกหนึ่งสำหรับ sondergard IocContainerคุณสามารถสร้างStandardKernelภายในApp-Class

เพียงแค่ลบ StartUpUri ออกจาก App.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>

นี่คือ CodeBehind ของแอพใน App.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();
    }
}

จากนี้ไป Ninject ยังมีชีวิตอยู่และพร้อมที่จะต่อสู้ :)

การฉีดไฟล์ DataContext

ในฐานะที่เป็น Ninject ยังมีชีวิตอยู่คุณสามารถดำเนินการทุกชนิดของการฉีดเช่นทรัพย์สิน Setter ฉีดหรืออย่างใดอย่างหนึ่งที่พบมากที่สุดสร้างฉีด

นี่คือวิธีที่คุณฉีด ViewModel ลงในWindow's ของคุณDataContext

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

แน่นอนว่าคุณสามารถใส่IViewModelถ้าคุณทำการผูกที่ถูกต้อง แต่นั่นไม่ใช่ส่วนหนึ่งของคำตอบนี้

เข้าถึงเคอร์เนลโดยตรง

หากคุณต้องการเรียกใช้ Methods บน Kernel โดยตรง (เช่น.Get<T>()-Method) คุณสามารถให้ Kernel ฉีดเข้าไปเอง

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

หากคุณต้องการอินสแตนซ์ภายในของเคอร์เนลคุณสามารถฉีดเป็นคุณสมบัติได้

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

แม้ว่าสิ่งนี้จะมีประโยชน์ แต่ฉันไม่แนะนำให้คุณทำเช่นนั้น เพียงสังเกตว่าวัตถุที่ฉีดด้วยวิธีนี้จะไม่สามารถใช้ได้ภายใน Constructor เนื่องจากถูกฉีดเข้าไปในภายหลัง

ตามลิงค์นี้คุณควรใช้ส่วนขยายจากโรงงานแทนการฉีดIKernel(DI Container)

แนวทางที่แนะนำในการใช้คอนเทนเนอร์ DI ในระบบซอฟต์แวร์คือ Composition Root ของแอปพลิเคชันเป็นที่เดียวที่สัมผัสกับคอนเทนเนอร์โดยตรง

วิธี Ninject.Extensions.Factory จะใช้ยังสามารถเป็นสีแดงที่นี่


แนวทางที่ดี ไม่เคยสำรวจ Ninject ถึงระดับนี้ แต่ฉันเห็นได้ว่าฉันพลาด :)
sondergard

@son thx. ท้ายคำตอบของคุณที่คุณระบุไว้หากใครมีวิธีที่ดีกว่านี้โปรดแชร์ คุณสามารถเพิ่มลิงค์นี้หรือไม่
LuckyLikey

หากมีใครสนใจเกี่ยวกับวิธีการใช้Ninject.Extensions.Factoryงานโปรดระบุไว้ที่นี่ในความคิดเห็นและฉันจะเพิ่มข้อมูลเพิ่มเติม
LuckyLikey

1
@LuckyLikey: ฉันจะเพิ่ม ViewModel ลงในหน้าต่างข้อมูลผ่าน XAML ซึ่งไม่มีตัวสร้างแบบไม่มีพารามิเตอร์ได้อย่างไร ด้วยวิธีแก้ปัญหาจาก sondergard ด้วย ServiceLocator สถานการณ์นี้จะเป็นไปได้
Thomas Geulen

ดังนั้นโปรดบอกวิธีเรียกใช้บริการที่ฉันต้องการในคุณสมบัติที่แนบมาด้วย? พวกเขาจะคงที่เสมอทั้งDependencyPropertyเขตข้อมูลสำรองและวิธีการรับและตั้งค่า
springy76

12

ฉันใช้วิธีการ "ดูก่อน" ซึ่งฉันส่งผ่านมุมมองโมเดลไปยังตัวสร้างของมุมมอง (ในโค้ดด้านหลัง) ซึ่งได้รับการกำหนดให้กับบริบทข้อมูลเช่น

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

        DataContext = viewModel;
    }
}

สิ่งนี้จะแทนที่แนวทางที่ใช้ XAML ของคุณ

ฉันใช้เฟรมเวิร์ก Prism เพื่อจัดการการนำทาง - เมื่อโค้ดบางรายการขอให้แสดงมุมมองเฉพาะ (โดย "การนำทาง" ไปที่มัน) ปริซึมจะแก้ไขมุมมองนั้น (ภายในโดยใช้เฟรมเวิร์ก DI ของแอพ) กรอบงาน DI จะแก้ไขการอ้างอิงใด ๆ ที่มุมมองมี (โมเดลมุมมองในตัวอย่างของฉัน) จากนั้นแก้ไขการอ้างอิงและอื่น ๆ

การเลือกเฟรมเวิร์ก DI ค่อนข้างไม่เกี่ยวข้องเนื่องจากทุกอย่างทำในสิ่งเดียวกันนั่นคือคุณลงทะเบียนอินเทอร์เฟซ (หรือประเภท) พร้อมกับประเภทคอนกรีตที่คุณต้องการให้เฟรมเวิร์กสร้างอินสแตนซ์เมื่อพบการพึ่งพาอินเทอร์เฟซนั้น สำหรับบันทึกฉันใช้ Castle Windsor

การนำทางด้วยปริซึมทำให้คุ้นเคย แต่ก็ค่อนข้างดีเมื่อคุณเข้าใจมันทำให้คุณสามารถเขียนแอปพลิเคชันของคุณโดยใช้มุมมองที่แตกต่างกัน เช่นคุณอาจสร้าง "ขอบเขต" ของปริซึมบนหน้าต่างหลักของคุณจากนั้นใช้การนำทางของปริซึมคุณจะเปลี่ยนจากมุมมองหนึ่งไปยังอีกมุมมองหนึ่งภายในภูมิภาคนี้เช่นเมื่อผู้ใช้เลือกรายการเมนูหรืออะไรก็ตาม

หรือดูที่เฟรมเวิร์ก MVVM เช่น MVVM Light ฉันไม่มีประสบการณ์เหล่านี้จึงไม่สามารถแสดงความคิดเห็นเกี่ยวกับสิ่งที่พวกเขาต้องการใช้


1
คุณส่งข้อโต้แย้งของตัวสร้างไปยังมุมมองเด็กได้อย่างไร ฉันได้ลองใช้วิธีนี้แล้ว แต่ได้รับข้อยกเว้นในมุมมองหลักที่บอกฉันว่ามุมมองเด็กไม่มีตัวสร้างเริ่มต้นที่ไม่มีพารามิเตอร์
Doctor Jones

10

ติดตั้ง MVVM Light

ส่วนหนึ่งของการติดตั้งคือการสร้าง view model locator นี่คือคลาสที่แสดงโมเดลมุมมองของคุณเป็นคุณสมบัติ ผู้ที่ได้รับคุณสมบัติเหล่านี้สามารถส่งคืนอินสแตนซ์จากเอ็นจิ้น IOC ของคุณได้ โชคดีที่ไฟ MVVM มีกรอบ 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' เพื่อรับ viewmodel ของคุณด้วยบริการฉีด

หวังว่าจะช่วยได้ ขออภัยในความไม่ถูกต้องของรหัสซึ่งเข้ารหัสจากหน่วยความจำบน iPad


คุณไม่ควรมีGetInstanceหรือresolveอยู่นอก bootstrap ของแอปพลิเคชัน นั่นคือประเด็นของ DI!
Soleil - Mathieu Prévot

ฉันยอมรับว่าคุณสามารถตั้งค่าคุณสมบัติระหว่างการเริ่มต้นได้ แต่การแนะนำว่าการใช้การสร้างอินสแตนซ์แบบขี้เกียจนั้นขัดกับ DI นั้นผิด
kidshaw

@kishaw ฉันไม่ได้
Soleil - Mathieu Prévot

3

เคส Canonic DryIoc

ตอบโพสต์เก่า แต่การทำเช่นนี้DryIocและทำในสิ่งที่ฉันคิดว่าเป็นการใช้ DI และอินเทอร์เฟซที่ดี (ใช้คลาสคอนกรีตน้อยที่สุด)

  1. จุดเริ่มต้นของแอป WPF คือApp.xamlและเราจะบอกว่าอะไรคือมุมมองพื้นฐานที่จะใช้ เราทำเช่นนั้นโดยใช้รหัสข้างหลังแทนที่จะเป็น xaml เริ่มต้น:
  2. ลบStartupUri="MainWindow.xaml"ใน App.xaml
  3. ใน codebehind (App.xaml.cs) ให้เพิ่มสิ่งนี้override OnStartup:

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

นั่นคือจุดเริ่มต้น นั่นเป็นที่เดียวที่resolveควรเรียก

  1. รูทการกำหนดค่า (อ้างอิงจากหนังสือของ Mark Seeman Dependency injection ใน. 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เท่านั้น
  • ฉันต้องระบุว่าจะใช้ contructor ตัวใด (เราต้องทำด้วย DryIoc) สำหรับ ViewModel เนื่องจากตัวสร้างเริ่มต้นจำเป็นต้องมีอยู่สำหรับตัวออกแบบ XAML และตัวสร้างที่มีการฉีดเป็นตัวสร้างจริงที่ใช้สำหรับแอปพลิเคชัน

ตัวสร้าง ViewModel พร้อม DI:

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

สรุป

ด้วยเหตุนี้เราจึงมีการใช้งานแอปพลิเคชัน WPF ที่สะอาดและน้อยที่สุดด้วยคอนเทนเนอร์ DryIoc และ DI ในขณะที่รักษาอินสแตนซ์การออกแบบของมุมมองและโมเดลมุมมองไว้ได้


2

ใช้การบริหารจัดการการขยายกรอบ

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

โดยทั่วไปสิ่งที่คุณจะทำคือมีคลาสแบบคงที่และใช้รูปแบบโรงงานเพื่อจัดเตรียมโกลบอลคอนเทนเนอร์ให้คุณ (แคช, นัทช์)

สำหรับวิธีการฉีดมุมมองคุณฉีดแบบเดียวกับที่คุณฉีดทุกอย่าง สร้างตัวสร้างการนำเข้า (หรือใส่คำสั่งนำเข้าในคุณสมบัติ / ฟิลด์) ในโค้ดด้านหลังของไฟล์ XAML และบอกให้อิมพอร์ตโมเดลมุมมอง แล้วผูกของคุณWindow's DataContextคุณสมบัติที่ว่า ออบเจ็กต์รูทของคุณที่คุณดึงออกมาจากคอนเทนเนอร์จริงๆมักจะเป็นอWindowอบเจ็กต์ประกอบ เพียงเพิ่มอินเทอร์เฟซให้กับคลาสหน้าต่างและส่งออกจากนั้นคว้าจากแค็ตตาล็อกด้านบน (ใน App.xaml.cs ... นั่นคือไฟล์ bootstrap WPF)


คุณกำลังขาดหายไปจุดหนึ่งที่สำคัญของการ DI ซึ่งก็คือการหลีกเลี่ยงการสร้างอินสแตนซ์ใด ๆ newกับ
Soleil - Mathieu Prévot

0

ฉันขอแนะนำให้ใช้ ViewModel - แนวทางแรก https://github.com/Caliburn-Micro/Caliburn.Micro

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

ใช้Castle Windsorเป็นคอนเทนเนอร์ IOC

ทั้งหมดเกี่ยวกับอนุสัญญา

หนึ่งในคุณสมบัติหลักของ Caliburn ไมโครเป็นสิ่งที่แสดงให้เห็นในความสามารถในการขจัดความต้องการรหัสแผ่นหม้อไอน้ำโดยทำตามข้อตกลงต่างๆ บางคนชอบการประชุมใหญ่และบางคนก็เกลียดการประชุมใหญ่ นั่นเป็นเหตุผลที่การประชุมของ CM สามารถปรับแต่งได้อย่างเต็มที่และยังสามารถปิดได้อย่างสมบูรณ์หากไม่ต้องการ หากคุณกำลังจะใช้อนุสัญญาและเนื่องจากมีการเปิดโดยค่าเริ่มต้นจึงควรทราบว่าอนุสัญญาเหล่านั้นคืออะไรและทำงานอย่างไร นั่นคือหัวข้อของบทความนี้ ดูความละเอียด (ViewModel-First)

พื้นฐาน

หลักการแรกที่คุณน่าจะพบเมื่อใช้ CM เกี่ยวข้องกับความละเอียดในการรับชม หลักการนี้มีผลต่อพื้นที่ ViewModel-First ของแอปพลิเคชันของคุณ ใน ViewModel-First เรามี ViewModel ที่มีอยู่แล้วซึ่งเราต้องใช้ในการแสดงผลบนหน้าจอ ในการทำเช่นนี้ CM ใช้รูปแบบการตั้งชื่อแบบง่ายเพื่อค้นหา UserControl1 ที่ควรผูกกับ ViewModel และการแสดงผล แล้วรูปแบบนั้นคืออะไร? ลองดูที่ 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);
};

ในตอนแรกเราจะไม่สนใจตัวแปร "บริบท" เพื่อให้ได้มาซึ่งมุมมองเราตั้งสมมติฐานว่าคุณกำลังใช้ข้อความ "ViewModel" ในการตั้งชื่อ VM ของคุณดังนั้นเราจึงเปลี่ยนเป็น "View" ทุกที่ที่พบโดยลบคำว่า "Model" ออก สิ่งนี้มีผลในการเปลี่ยนทั้งชื่อชนิดและเนมสเปซ ดังนั้น ViewModels.CustomerViewModel จะกลายเป็น Views.CustomerView หรือถ้าคุณจัดระเบียบแอปพลิเคชันของคุณตามคุณสมบัติ: CustomerManagement.CustomerViewModel กลายเป็น CustomerManagement.CustomerView หวังว่าจะตรงไปตรงมา เมื่อได้ชื่อแล้วเราก็ค้นหาประเภทที่มีชื่อนั้น เราค้นหาแอสเซมบลีใด ๆ ที่คุณสัมผัสกับ CM ตามที่สามารถค้นหาได้ผ่าน AssemblySource.Instance.2 หากเราพบประเภทเราจะสร้างอินสแตนซ์ (หรือรับจากคอนเทนเนอร์ IoC หากมีการลงทะเบียน) และส่งคืนให้กับผู้เรียก หากเราไม่พบประเภท

ตอนนี้กลับไปที่ค่า "บริบท" นั้น นี่คือวิธีที่ CM รองรับหลายมุมมองบน ViewModel เดียวกัน หากมีการระบุบริบท (โดยทั่วไปจะเป็นสตริงหรือ enum) เราจะทำการแปลงชื่อเพิ่มเติมโดยยึดตามค่านั้น การเปลี่ยนแปลงนี้ถือว่าคุณมีโฟลเดอร์ (เนมสเปซ) สำหรับมุมมองที่แตกต่างกันอย่างมีประสิทธิภาพโดยลบคำว่า "View" ออกจากท้ายและต่อท้ายบริบทแทน ดังนั้นเมื่อพิจารณาบริบทของ“ Master” ViewModels ของเรา CustomerViewModel จะกลายเป็น Views.Customer.Master


2
โพสต์ทั้งหมดของคุณเป็นความคิดเห็น
John Peters

-1

ลบ uri เริ่มต้นออกจาก app.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);
    }
}

ตอนนี้คุณสามารถใช้คลาส IoC ของคุณเพื่อสร้างอินสแตนซ์

MainWindowView.xaml.cs

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

        //Do other configuration            

        DataContext = mainWindowViewModel;

        InitializeComponent();
    }

}

คุณไม่ควรมีคอนเทนเนอร์GetInstanceของ resolveapp.xaml.cs ภายนอกคุณกำลังสูญเสียจุดของ DI นอกจากนี้การกล่าวถึงมุมมอง xaml ในโค้ดด้านหลังของมุมมองเป็นเรื่องที่ซับซ้อน เพียงแค่เรียกมุมมองใน c # บริสุทธิ์และทำสิ่งนี้กับคอนเทนเนอร์
Soleil - Mathieu Prévot
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.