Multiple instances of viewmodel

Jan 12, 2015 at 9:07 PM
Hi,

how do I create multiple instances of the same usercontrol? I have a listview and a tabcontrol. When I click a listviewitem a new tab gets created with the usercontrol as content. However when I click multiple listviewitems, all the tabs are synced with the last selected listviewitem.

How can I prevent this and make each usercontrol unique?

I am using mvvm light and wpf

thanks
Jan 14, 2015 at 5:43 AM
Nobody that can help me ? I have asked the same question on stackoverflow but I don't get an answer there.

I have read about the fact that you can unique initialize your viewmodel in the viewmodellocator by adding a Guid to it:
    public DossierDetailViewModel DossierDetail
    {
        get
        {
            //return ServiceLocator.Current.GetInstance<DossierDetailViewModel>();
            return SimpleIoc.Default.GetInstance<DossierDetailViewModel>(Guid.NewGuid().ToString());
            //return new DossierDetailViewModel();
        }
    }
but I still have the same problem.

Any help is appreciated
Coordinator
Jan 14, 2015 at 5:59 AM
Hi,

In this situation, I would assign the datacontext in code behind when you create the new tab (assuming that the tab creation is done in code).

You can use the IOC container to create the VM if you need to keep track of it in the registry. If not, you can just get it from a factory method on the main VM.

Then create your Tab and use something like

myTab.DataContext = theNewVm;

Does it make sense?
Cheers
Laurent
Jan 14, 2015 at 6:43 AM
Hi Laurent, thank you for your help.

The tab is indeed created in code behind. It is created with each click of a listviewitem and added to the tabcontrol. The content of the tab is a usercontrol with its own viewmodel. When I click a few listviewitems then more tabs are created but all with the same usercontrol (and thus viewmodel).

This is the XAML of the TabControl:
        <TabControl Grid.Row="0" Grid.Column="1"
            ItemsSource="{Binding Path=OpenDossiers}" SelectedIndex="{Binding SelectedIndex}">
            <TabControl.ItemTemplate>
                <DataTemplate>
                    <WrapPanel>
                        <TextBlock FontSize="18" Text="{Binding TabName}"/>
                        <Button Command="{Binding CloseDossierCommand}" Content="X" Margin="4,0,0,0" FontFamily="Courier New" Width="17" Height="17" VerticalContentAlignment="Center" />
                    </WrapPanel>
                </DataTemplate>
            </TabControl.ItemTemplate>
            <TabControl.ContentTemplate>
                <DataTemplate>
                    <views:DossierDetailView />
                </DataTemplate>
            </TabControl.ContentTemplate>
        </TabControl>
It's itemsource is OpenDossiers which is an observablecollection:
    private ObservableCollection<DossierDetailViewModel> _openDossiers;

    public ObservableCollection<DossierDetailViewModel> OpenDossiers
    {
        get
        {
            return _openDossiers;
        }
        set
        {
            _openDossiers = value;
            RaisePropertyChanged("OpenDossiers");
        }
    }
The creation of a new tab is done like this:
            DossierDetailViewModel newDossier = new DossierDetailViewModel();
            newDossier.TabName = SelectedDossier.Omschrijving;
            this.OpenDossiers.Add(newDossier);
            Messenger.Default.Send<DTO.Dossier.Dossier>(SelectedDossier, "SetDossier");
            SelectedIndex = OpenDossiers.IndexOf(newDossier);
As you can see the DossierDetailViewModel is the viewmodel that is used in each tab. The Messenger.Send fills a property of that viewmodel with the selected listviewitem. But when I have multiple tabs and so multiple instances of the usercontrols with the viewmodel every tabs is synced with the last selected listviewitem.

I have tried it with TabItem and DataContext as you suggested but that didn't make a difference. Maybe you can see something wrong in my code or way of thinking :)
Coordinator
Jan 14, 2015 at 7:08 AM
Hi

At first glance it should work since the datacontext of the DataTemplate (of the TabControl) will be set to the current item in the ObsCollection.

That said I am looking at that on a mobile phone so maybe I missed something. In this scenario, setting the DataContext in code won't help you since it is automatically done at the Data Template level, and then inherited down to the user control.

Do you in any place set the DataContext of the User control itself?

I'll take a closer look at home.

Cheers
Laurent
Jan 14, 2015 at 7:30 AM
Edited Jan 14, 2015 at 7:31 AM
I appreciate your valuable time Laurent.

This is the xaml of my usercontrol. As you can see it has the datacontext of the viewmodel. As a quick test I put the Dossier.Omschrijving (which is a part of the object the usercontrol gets from the selected listviewitem) in a textbox:

<UserControl x:Class="eDossier.WPF.View.DossierDetailView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"             
    xmlns:ignore="http://www.ignore.com"
    mc:Ignorable="d ignore"
    DataContext="{Binding DossierDetail, Source={StaticResource Locator}}">

<Grid >
    <ScrollViewer >
        <StackPanel Grid.Row="2" Grid.Column="0" Margin="0,0,0,5">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Naam van het dossier" Margin="5" Grid.Column="0" />
                <TextBox Grid.Column="1" Text="{Binding Path=Dossier.Omschrijving}" Controls:TextBoxHelper.Watermark="Omschrijving van het dossier"/>
            </Grid>
        </StackPanel>
    </ScrollViewer>
</Grid>
</UserControl>

With in its viewmodel the property "Dossier" which holds the selected listviewitem and "TabName" which holds the name of the tab:
    private DTO.Dossier.Dossier _Dossier;
    public DTO.Dossier.Dossier Dossier
    {
        get { return _Dossier; }
        set
        {
            _Dossier = value;
            RaisePropertyChanged("Dossier");
        }
    }

    private string _tabName;
    public string TabName
    {
        get { return _tabName; }
        set
        {
            _tabName = value;
            RaisePropertyChanged("TabName");
        }
    }
And the registration of the "SetDossier" Messenger which fills the selected listviewitem to the property "Dossier":
    public DossierDetailViewModel()
    {
        Messenger.Default.Register<DTO.Dossier.Dossier>(this, "SetDossier", (dossier) =>
                                           SetDossier(dossier)
                                             );
    }
Just to be complete, here is my viewmodellocator (only with the code needed for this problem):
    public ViewModelLocator()
    {
        ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

        //Views
        SimpleIoc.Default.Register<DossierDetailViewModel>();

    }

    public DossierDetailViewModel DossierDetail
    {
        get
        {
            return SimpleIoc.Default.GetInstance<DossierDetailViewModel>(Guid.NewGuid().ToString());
        }
    }

Thank you
Coordinator
Jan 14, 2015 at 8:52 AM
Hi,

Yes there is a flaw in your understanding of what is happening at the XAML level.

in XAML, the DataContext is inherited down to the element's children. This is why, when you have (for instance) a Windows with the DataContext set, you can go to a TextBlock in this window and set Text="{Binding MyProperty}". This will automatically refer to the MyProperty on the current DataContext.

There are two kinds of viewmodels in an application. Some are larger, long lived VMs. For instance MainViewModel, SettingsViewModel, etc. These are suitable for creation and lifetime management in the ViewModelLocator (VML).

But there are also some VMs that are more transient, shorter lived, and these should probably not appear as properties in the VML. Your data items are this kind of VMs. There are typically multiple instances of these smaller, shorter lived VMs. I call them data VMs.

In your case, your tab control's source is an ObservableCollection. Thanks to the CollectionChanged even, your UI will auto update. And thanks to the DataTemplate, the DataContext will automatically be set to the current item of the ObsCollection. This is a cool feature because you don't need to set the DataContext explicitly on the DataTemplate! In fact in most cases it is (as you found out here) impossible to set the DataContext explicity, because you don't know how to identify the right item in the collection.

So in your case, what you need to do is this:
  • Remove the property in the VML. It is useless there because if doesn't provide you the right item for the current UserCOntrol.
  • Remove the DataContext assignment in the XAML. Instead, you will simply inherit the DataContext from the usercontrol's parent, which is the DataTemplate
  • If you need design time features (in Blend or in Visual Studio), you can use the d:DataContext (design time DataContext). Check my MSDN article here: http://msdn.microsoft.com/en-us/magazine/dn169081.aspx
Then the usercontrol's DataContext will automatically be set to the ObservableCollection's item, which is the current instance of the DossierDetailViewModel.

Hopefully this helps.

Cheers
Laurent
Marked as answer by Sergedb on 1/14/2015 at 2:32 AM
Jan 14, 2015 at 9:31 AM
Edited Jan 14, 2015 at 9:44 AM
Yes ! Spot on..thank you for your brief explanation about the XAML DataContext and escpecially about the 2 kinds of viewmodels. I thought all viewmodels had to be put in the viewmodellocator. Thanks to your info I see the difference now between the 2!

Ok so I removed the property from the VML and removed the DataContext assignment as it is inherited by the parent (DataTemplate).

I had to do one more thing which is normal. I removed the Messenger register and send which filled the Dossier property. Because all usercontrols where registered to the same Messenger, the Dossier property always got the last one.

Thank you for your time as your time probably is very valuable. I hope someday I could do the same for you ;-)

Serge