Search code examples
c#autofacreactiveuiavaloniaui

How to use ViewLocation with multiple views for the same type with ReactiveUI?


I am using ViewLocation feature of ReactiveUI with splat autofac for autowiring views with view models. Is it possible to have multiple views for the same type, and choose which one to use based on some parameter?

Here is a simple example of what I would like to do:

public class Person
{
    public string Name {get; set;}
    public int Age {get; set;}
}

View #1 for Person class:

<UserControl
    ...namespaces...>
    <TextBox Name="NameTextBox"/>
</UserControl>

and code behind

public partial class PersonNameView : ReactiveUserControl<Person>
{
    public PersonNameView()
    {
        this.WhenActivated(disposables => {
            this.Bind(ViewModel, vm => vm.Name, v => v.NameTextBox.Text).DisposeWith(disposables);
        });
        InitializeComponent();
    }
}

View #2 for Person class:

<UserControl
    ...namespaces...>
    <TextBox Name="AgeTextBox"/>
</UserControl>

and code behind

public partial class PersonAgeView : ReactiveUserControl<Person>
{
    public PersonAgeView()
    {
        this.WhenActivated(disposables => {
            this.Bind(ViewModel, vm => vm.Age, v => v.AgeTextBox.Text).DisposeWith(disposables);
        });
        InitializeComponent();
    }
}

Somewhere In the main view there would be a collection of Persons:

<!--other code-->
<ItemsControl Name=PersonItemsControl/>

and in code behind

public partial class MainView : ReactiveUserControl<MainViewModel>
{
    public MainView()
    {
        //other code
        this.WhenActivated(disposables => {
            this.OneWayBind(ViewModel, vm => vm.Persons, v => v.PersonItemsControl.ItemsSource).DisposeWith(disposables);
        });
        InitializeComponent();
    }
}

Both Views are registered with autofac:

builder.RegisterType<PersonNameView>().As<IViewFor<Person>>().InstancePerDependency();
builder.RegisterType<PersonAgeView>().As<IViewFor<Person>>().InstancePerDependency();

If my observation is correct if multiple views are registered for the same type, the last registered view is chosen (PersonAgeView in this case). But is there a way to choose which one is used?

Keep in mind this is very simplified example.


Solution

  • It is possible as Lukasz suggested with contracts (his answer led to this solution). I'm not sure if it is possible with autofac or other DI implementations for ReactiveUI because I found no way to register contract. So i switched to native splat DI. With splat you have two options.

    #1 when manually registering views there is a contract parameter

    Locator.CurrentMutable.Register(() => new PersonNameView(), typeof(IViewFor<Person>));
    Locator.CurrentMutable.Register(() => new PersonAgeView(), typeof(IViewFor<Person>), contract: "age");
    

    #2 when using Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly()); to automatically register views, you can use attribute ViewContract to decorate view.

    [ViewContract("age")]
    public partial class PersonAgeView : ReactiveUserControl<Person>
    {
        public PersonAgeView()
        {
            this.WhenActivated(disposables => {
                this.Bind(ViewModel, vm => vm.Age, v => v.AgeTextBox.Text).DisposeWith(disposables);
            });
            InitializeComponent();
        }
    }
    

    For some reason you cannot use ViewContract attribute when manually registering views so be mindful of that.

    To choose which registration to use, you have to use ViewModelViewHost inside your view and provide contract info.

    namespace:

    xmlns:reactive="using:Avalonia.ReactiveUI"
    

    view:

    <!--other code-->
    <ItemsControl
        Name="PersonItemsControl">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <reactive:ViewModelViewHost
                    ViewModel="{Binding .}"
                    ViewContract="age"/>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>