Inject views into ItemsControl depending on object

2019-05-14 18:51发布

问题:

I have a service returning an array of type Party. Party has two subtypes, Person and Organization. I’m consuming this service in my WPF application (Prism, MVVM) from a view model. In the constructor of this view model I populate an observable collection of type Party:

public PhoneBookViewModel(IPhoneBookService phoneBookProxy)
{
    _phoneBookProxy = phoneBookProxy;

    var parties = _phoneBookProxy.GetAllParties();
    _parties = new ObservableCollection<Party>(parties.ToList());
}

So far so good. In my PhoneBookView I have an ItemsControl that binds to this collection. In this control I want to render each Party by using another View (and its view model). So when Party is of type Person, inject PersonView and pass the Party object to the constructor of the PersonViewModel, and when Party is of type Organization, render OrganizationView, and so on... You get the picture (or?).

But I can't figure out how to do this in XAML. Any ideas? This is probaly not the best way of doing it, so if you can recommend a better approach, please enlighten me :-)

Thanks!

回答1:

Lets examine this from the view towards the model:


Lets assume we have 2 different types of views, 1 type of view model:

ViewA --> Created within an items control using DataTempate/DataTemplateSelector, Binded > to ViewModelA

ViewB --> Created within an items control using DataTempate/DataTemplateSelector, Binded to ViewModelA

If both views are binded to the same view model, you would end up with the same view.


Lets try again with 2 different types of views and 2 different types of view models:

ViewA --> Created within an items control using DataTempate/DataTemplateSelector, Binded to ViewModelA --> Binded to ModelA

ViewB --> Created within an items control using DataTempate/DataTemplateSelector, Binded to ViewModelB --> Binded to ModelB

This IS possible.


Now if you model your view-models and models like this (pseudo code):

public PhoneBookViewModel
{
    public PhoneBookViewModel()
    {
        _parties = new ObservalbeCollection<PartyViewModel>();
    }

    private PhoneBook _dataContext;

    // This is the property the VM uses to access the model
    public PhoneBook DataContext
    {
        get { return _dataContext; }
        set
        {
            if (_dataContext != null)
            {
                _dataContext.Parties.CollectionChanged -= OnModelPartiesChanged;
            }
            _dataContext = value;
            if (_dataContext != null)
            {
                _dataContext.Parties.CollectionChanged += OnModelPartiesChanged;
            }
        }
    }

    private ObservableCollection<PartyViewModel> _parties;

    // This is the property the view uses to access the collection of VM parties
    public ObservableCollection<PartyViewModel> PartiesViewModels { get { return _parties; } }

    private void OnModelPartiesChanged(...)
    {
        // Add/remove VMs to/from PartiesViewModels here
    }
}

// Model
public PhoneBook
{
    public PhoneBook()
    {
        _parties = new ObservalbeCollection<Party>();
    }

    private ObservableCollection<Party> _parties;

    // This is the property the VM uses to access the model's parties
    public ObservableCollection<Party> Parties { get { return _parties; } }
}

public PersonViewModel : PartyViewModel
{
    new Person DataContext { get; set; }
}

public PartyViewModel
{
    public Party DataContext { get; set; }
}

then you will get correct type of VMs for each model item, view will be binded to VM items, not model items.


View's datatemplates:

<DataTemplate x:Target={x:Type myVmNamespace:PersonViewModel}">
    <PersonView/>
</DataTemplate>

<DataTemplate x:Target={x:Type myVmNamespace:GroupViewModel}">
    <GroupView/>
</DataTemplate>

View's itemscontrol:

<!-- Bind to Parties property of PhoneBookVM -->
<!-- Uses datatemplates for items -->
<ListView ItemsSource={Binding Parties}"/>


回答2:

Configure a data template for your data-types to render the xaml.



回答3:

If you're using Prism and MVVM then you are binding a Command to your ItemsControl with all the Parties.

This command would be of type DelegateCommand<Party>. Inside the Delegate that the command is executing, which would look like:

private void PartyNavigate(Party party)

Just check if the party is any of the sub types and call a RequestNavigate on the region of your RegionManager to the specific View.

It will then become a problem how you pass the actual context, you can either look at the MVVM RI that comes with Prism, which has a very good aproach to that in the form of the StateHandler or you can build your own centralized DataManager where you keep state of those things, along with caching stuff you are getting from WebServices, etc. After 2 years of building smart clients with WPF and WCF I can tell you that you will eventually need to build your own DataManager, which won't be that much of a deal if you're already using EntityFramework and you generate most of it from the EDM.



回答4:

Your collection for your ItemsControl ItemsSource is filled by PhoneBookViewModel. So the only thing left is to tell WPF how each item of this collection should be rendered. And this can easy achieved by creating a DataTemplate.

 <DataTemplate DataType="{x:Type PersonViewModel}"> 
     <MyPersonView/> 
 </DataTemplate>