WPF + Castle Windsor + MVVM: Locator-DataContext

2020-07-31 21:28发布

问题:

Edit:
I have found one method to do this but I'm not sure if it is the best way.
In WindsorContainer initialization, first I register viewmodel:

container.Register(Component.For<CentrosViewModel>().LifeStyle.Transient);

and later I register the View

        container.Register(Component.For<CentrosAdminView>().LifeStyle.Transient.DependsOn(Property.ForKey("DataContext")
            .Eq(ViewModelLocator.Centrosviewmodel)));

And definition of property ViewModelLocator.Centrosviewmodel is:

    public static CentrosModel Centrosviewmodel
    {
        get
        {
            return App.container.Resolve<CentrosViewModel>();
        }
    }

End Edit

I'm trying to make an Wpf application using Castle Windsor and Mvvm Toolkit (galasoft) but I thing my problem will be the same with any MVVM toolkit.

With MVVM you must set the DataContext of the View to your ViewModel. Normally this is done by something like this in declaration of the view

DataContext={Binding MyViewModelInstanceName,Source={StaticResource Locator}}

Resource Locator is defined in App.xaml like this:

<Application.Resources>
    <!--Global View Model Locator-->
    <vm:ViewModelLocator x:Key="Locator" />
</Application.Resources>

If I establish StartupURI in App.xaml to my view all is right. But if I leave StartupUri blank and I try to get an instance of my view through castle using following syntax:

container.Resolve<CentrosAdminView>().Show();

I get exception: "Cannot Find Resource with Name '{Locator}'

I supose that Initial DataContext is different when running directly than when running through Castle Windsor and this is the reason why it can't find resource.

My two questions are:

  • Is It necessary have a ViewModelLocator when using Castle Windsor? In
  • case of Yes: How can I setup DataContext of Views correctly with
  • Windsor? In case of No: How would be the right way?

I leave down my Castle Configuration. Any help would be really appreciated.

My Windsor configuration look like this:

<castle>
    <properties>
      <!-- SQL Server settings -->
      <connectionString>Server=192.168.69.69;Database=GIOFACTMVVM;user id=sa;password=22336655</connectionString>
      <nhibernateDriver>NHibernate.Driver.SqlClientDriver</nhibernateDriver>
      <nhibernateDialect>NHibernate.Dialect.MsSql2005Dialect</nhibernateDialect>
    </properties>

    <facilities>

      <facility id="nhibernatefacility"
                type="Repository.Infrastructure.ContextualNHibernateFacility, Repository">

        <factory id="sessionFactory1">
          <settings>
            <item key="connection.provider">NHibernate.Connection.DriverConnectionProvider</item>
            <item key="connection.driver_class">#{nhibernateDriver}</item>
            <item key="connection.connection_string">#{connectionString}</item>
            <item key="dialect">#{nhibernateDialect}</item>
            <item key="proxyfactory.factory_class">NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle</item>
          </settings>
          <assemblies>
            <assembly>Domain</assembly>
            <assembly>ObservableCollections</assembly>
          </assemblies>
        </factory>

      </facility>
    </facilities>
  </castle>

and by code:

    public static IWindsorContainer Start()
    {
        var container = new WindsorContainer(new XmlInterpreter());

        container.AddFacility<TransactionFacility>();

        container.Register(
            Component.For<HistoriasAdminView>().LifeStyle.Transient,
            Component.For<HistoriasModel>().LifeStyle.Transient,
            Component.For<CentrosModel>().LifeStyle.Transient,
            Component.For<CentrosAdminView>().LifeStyle.Transient, 
            Component.For<MainViewModel>().LifeStyle.Transient,
            Component.For<MainWindow>().LifeStyle.Transient, 

            Component.For<IRepository<Historias>>().ImplementedBy<Repository<Historias>>().LifeStyle.Transient, 
            Component.For<IRepository<Centros>>().ImplementedBy<Repository<Centros>>().LifeStyle.Transient, 
            Component.For<IUnitOfWork>().ImplementedBy<NHUnitOfWork>().LifeStyle.Transient 
            );

        return container;
    }

回答1:

You are using the Service Locator pattern, where you register services, pass around a reference to the container, and explicitly call resolve all over you code. If you take a quick tour through the Castle Windsor wiki, they discourage this use of the container.

Generally, you should register all your types (via installers), resolve only one root object (maybe your main view, maybe some sort of startup/MVC controller style code), and let the rest be resolved by the container. The next time you will call the container will almost always be container.Dispose, when your application exits.

See the Windsor wiki page regarding the Three Calls Pattern.

If you find cases where you need to pull from the container during runtime to create specific instances (where you have to pass specific parameters to create that instance), use the Typed Factory Facility instead of directly resolving dependencies.

MVVM with Windsor:

In my first MVVM app I wrote with Windsor (just finished the first version), I simply registered my main view and view model without specifying the lifestyle. This defaulted them to be singletons.

The view took a view model instance as a required dependency (in the constructor), and used it to set the data context. I did this in code-behind because it was non-intrusive and painless.

// In my program I used interfaces for everything.  You don't actually have to...
public interface IMainView
{
    void Show();
}

public class MainView : Window, IMainView
{
    public MainView(IMainViewModel viewModel)
    {
        Initialize();
        this.DataContext = viewModel;
    }
}

public interface IMainViewModel
{
    int SomeProperty { get; set; }
    ICommand ShowSubViewCommand { get; }
    // ...
}

public class MainViewModel : IMainViewModel
{
    public MainViewModel(SomeOtherSubComponent subComponent)
    {
        this.subComponent = subComponent;
        // ...
    }

    // ...
}

Where I had sub-views that I wanted to create multiple instances of, I registered them with a transient life-cycle. Then I created a view factory and view model factory, and used them to get instances of the sub-view and sub-viewmodel from the parent view. I registered an event handler for the view's close event, and called a Release method on the factory classes.

public interface ISubView
{
    void Show();
    event Action OnDismissed;
}

public class SubView : Window, ISubView
{
    public SubView(ISubViewModel viewModel)
    {
        Initialize();
        this.DataContext = viewModel;
        // Would do this in the view model,
        // but it is a pain to get Window.Close to call an ICommand, ala MVVM
        this.OnClose += (s, a) => RaiseDismissed();
    }

    public event Action OnDismissed;

    private void RaiseDismissed()
    {
        if(OnDismissed != null)
            OnDismissed();
    }
}

public interface ISubViewModel
{
    string SomeProperty { get; }
    // ...
}

// Need to create instances on the fly, so using Typed Factory facility.
// The facility implements them, so we don't have to :)
public interface IViewFactory
{
    ISubView GetSubView(ISubViewModel viewModel);
    void Release(ISubView view);
}

public interface IViewModelFactory
{
    ISubViewModel GetSubViewModel();
    void Release(ISubViewModel viewModel);
}

// Editing the earlier class for an example...
public class MainViewModel : IMainViewModel
{
    public MainViewModel(IViewFactory viewFactory, IViewModelFactory viewModelFactory)
    {
        this.viewFactory = viewFactory;
        this.viewModelFactory = viewModelFactory;
        // Todo: Wire up ShowSubViewCommand to call CreateSubView here in ctor
    }

    public ICommand ShowSubViewCommand { get; private set; }

    private void CreateSubView()
    {
        var viewModel = viewModelFactory.GetSubViewModel();
        var view = viewFactory.GetSubView(viewModel);

        view.OnDismissed += () =>
        {
            viewModelFactory.Release(viewModel);
            viewFactory.Release(view);
        };

        view.Show();
    }

    // Other code, private state, etc...
}

At the end of all this, the only calls to the container are:

void App_Startup()
{
    this.container = new WindsorContainer();
    container.Install(Configuration.FromAppConfig());

    var mainView = container.Resolve<IMainView>();
    mainView.Show();
}

public override OnExit()
{
    container.Dispose();
}

The benefit to all this rigmarole is that it is independent of the container (and can be used without a container), it is obvious which dependencies each of my components take, and most of my code doesn't ever have to ask for its dependencies. The dependencies are just handed to each component as it needs it.