Shared viewmodel between two Views in WPF MVVM pattern
Asked Answered
A

2

1

I'm trying to find a technique to show modal views from within other views but I am having problems. Here's a simple example of what i'm trying to do:

Shared ViewModel

class ClientesViewModel : Screen
{
    private bool _deleteconfirmvisible;
    public bool DeleteConfirmVisible
    {
        get { return _deleteconfirmvisible; }
        set
        {
            _deleteconfirmvisible = value;
            NotifyOfPropertyChange("DeleteConfirmVisible");
        }
    }

    public void ShowDeleteConfirm()
    {
        this.DeleteConfirmVisible = true;
    }

    public ModalViewModel ModalDelete
    {
        get { return new ModalViewModel(); }            
    }

    public void ConfirmDelete()
    {
        //Actually delete the record
        //WCFService.DeleteRecord(Record)
    }
}

First View

<UserControl x:Class="Ohmio.Client.ClientesView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
             xmlns:local="clr-namespace:Ohmio.Client"   
             mc:Ignorable="d" 
             d:DesignHeight="364" d:DesignWidth="792">

    <UserControl.Resources>
        <DataTemplate DataType="{x:Type local:ModalViewModel}">
            <local:ModalView/>
        </DataTemplate>
    </UserControl.Resources>
    <local:ModalContentPresenter DataContext="{Binding}" IsModal="{Binding DeleteConfirmVisible}" Grid.ColumnSpan="5" Grid.RowSpan="4" ModalContent="{Binding Path=ModalDelete}">
        <Grid>        
            <Button x:Name="ShowDeleteConfirm" Margin="5" Grid.Column="2" Content="Delete Record"/>        
        </Grid>
    </local:ModalContentPresenter>
</UserControl>

Second View (Modal content)

<UserControl x:Class="Ohmio.Client.ModalView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Ohmio.Client"   
             mc:Ignorable="d" Height="145" Width="476">
    <Grid>        
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>            
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="50"></RowDefinition>
        </Grid.RowDefinitions>        
        <Label Content="Are you sure you want to delete this record?" Grid.ColumnSpan="2" HorizontalAlignment="Center" VerticalAlignment="Center"></Label>
        <Button x:Name="ConfirmDelete" IsDefault="True" Grid.Row="2" Content="Aceptar" Margin="10"></Button>
        <Button x:Name="TryClose" IsCancel="True"  Grid.Row="2" Grid.Column="1" Content="Cancelar" Margin="10"></Button>        
    </Grid>        
</UserControl>

ModalViewModel

class ModalViewModel : Screen
{        
    public ModalViewModel()
    {

    }        
}

So the basic idea is to have two views that share the same viewmodel. This viewmodel has properties for showing the modal content and deleting the record.

The problema here is that ConfirmDelete method is never called.I guess the problem is that the child views DataContext is different from parent view. So How can I Solve this?

Thanks!

EDIT

Forgot to mention, i'm using Caliburn.Micro

EDIT 2

I follow Rachel suggest and divide the viewmodel. Still the problem pesist. Here is how my code looks like now:

TestViewModel

class TestViewModel :Screen
    {
        private bool _deleteconfirmvisible;
        TestModalViewModel _modaldelete;

        public TestViewModel()
        {
            _modaldelete = new TestModalViewModel();
        }
        public bool DeleteConfirmVisible
        {
            get { return _deleteconfirmvisible; }
            set
            {
                _deleteconfirmvisible = value;
                NotifyOfPropertyChange("DeleteConfirmVisible");
            }
        }

        public void ShowDeleteConfirm()
        {
            this.DeleteConfirmVisible = true;
        }

        public TestModalViewModel ModalDelete
        {
            get { return _modaldelete; }
        }
    }

TestView

<UserControl x:Class="Ohmio.Client.TestView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
             xmlns:local="clr-namespace:Ohmio.Client"   
             mc:Ignorable="d" 
             d:DesignHeight="364" d:DesignWidth="792">
    <UserControl.Resources>
        <DataTemplate DataType="{x:Type local:TestModalViewModel}">
            <local:TestModalView/>
        </DataTemplate>
    </UserControl.Resources>    
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="*"></RowDefinition>
                <RowDefinition Height="40"></RowDefinition>
            </Grid.RowDefinitions>
        <ContentPresenter Content="{Binding Path=ModalDelete}"></ContentPresenter>
        <Button x:Name="ShowDeleteConfirm" Margin="5" Grid.Row="1" Content="Delete Record"/>
        </Grid>    
</UserControl>

TestModalViewModel

class TestModalViewModel : Screen
    {
        private Boolean _result;

        public TestModalViewModel()
        {
            _result = false;
        }        

        public void ConfirmAction()
        {
            _result = true;
            TryClose();
        }        

        public bool Result
        {
            get { return _result; }            
        }
    }    

TestModalView

<UserControl x:Class="Ohmio.Client.TestModalView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Ohmio.Client"   
             mc:Ignorable="d" Height="145" Width="476">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="50"></RowDefinition>            
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>

        <Label Content="Are you sure you want to delete this record?" Grid.ColumnSpan="2" HorizontalAlignment="Center" VerticalAlignment="Center"></Label>        
        <Button x:Name="ConfirmAction" IsDefault="True" Grid.Row="2" Content="Aceptar" Margin="10"></Button>
        <Button x:Name="TryClose" IsCancel="True"  Grid.Row="2" Grid.Column="1" Content="Cancelar" Margin="10"></Button>
    </Grid>
</UserControl>

I change the ModalContentPresenter for and ContentPresenter, and the problema remains: ConfirmAction is never called, and i can't understand why. Can anyone tell me why?

EDIT 3

Snoop Result:

Snoop Result

Acrid answered 13/4, 2015 at 14:28 Comment(7)
I think you have misunderstood how buttons hook up to commands. Have a look hereMayne
@user1 I'm going to hazard a guess that given the VM derives from Screen that this is Caliburn Micro. Caliburn will magically bind the button to a method with the same name as the button.Roath
Thanks for your comment. Forgot to mention i'm using Caliburn.Micro so i don't need to explicit créate ICommandAcrid
Little bit confused about view arrangement. Why are you trying to put ShowDeleteConfirm button inside the ModalContentPresenter?Atelier
Thanks for your interest. I have one view, with a Delete button. When i Clicked a new view is show on top of it (modal) with two buttons "Are you sure (yes/no)". So if i click Yes, the modal should close and actual delete must execute.Acrid
@Acrid Can you use a tool like Snoop to double-check the .DataContext is correct behind your TestModelView?Chrismatory
@Chrismatory yes I Can confirm that the dataContext is set (as long as I can see). I'll attach a printscreen.Acrid
C
1

When you use an implicit DataTemplate, WPF automatically sets the .DataContext property to whatever the data object was.

<DataTemplate DataType="{x:Type local:ModalViewModel}">
    <local:ModalView/> <!-- DataContext is set to the ModelViewModel object -->
</DataTemplate>

So any instance of your ModelView control will have it's .DataContext set to a ModelViewModel object for binding purposes.

You could change your specific bindings to point to a different Source than the current DataContext, like this :

<Button x:Name="ConfirmDelete" 
        Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:ModalContentPresenter}}, 
                          Path=DataContext.ConfirmDelete }" ... />

however this is not ideal since it relies on the developer knowing to use a specific UserControl structure, such as making sure ModelView is always nested inside of a ModelContentPresenter controls

A better solution is to ensure your code is properly separated, and the ModelView only has to worry about displaying the Model, while other code is in another part of the template.

<!-- This layer is used to display the entire ClientesViewModel object -->
<UserControl>

    <!-- this UserControl is only responsible for displaying the ModalViewModel object -->
    <UserControl> 
        <!-- However ModelViewModel should look... -->
        <Label Content="Are you sure you want to delete this record?" ... />
    </UserControl>

    <!-- DataContext here is ClientesViewModel, so these bindings work -->  
    <Button Content="Aceptar" Command="{Binding ConfirmDelete}" ... /> 
    <Button Content="Cancelar" Command="{Binding TryClose}" ... />

</UserControl>     

You can easily have more than one UserControl for the same data object. One for the Clients screen, and one for the Delete dialog screen.

Although the best solution would probably be to separate your code properly so all delete code is in one object, and all Clients code is in another

<!-- This layer is used to display the entire ClientesViewModel object -->
<local:ClientsView>

    <!-- this UserControl is only responsible for displaying the ModalDelete object -->
    <local:DeleteView />

</local:ClientsView>

and

class ClientesViewModel
{
    bool DeleteConfirmVisible;
    void ShowDeleteConfirm();
    ModalViewModel ModalDelete;
}

public class ModalViewModel
{
    ICommand ConfirmDelete;
    ICommand TryClose;
}

EDIT

Based on the update to your question, my best guess is it is a problem with the implementation of Caliburn Micro's automagical bindings. I've never used Caliburn Micro before, so I'm not sure I can help you there.

A quick google search suggests it may have something to do with the fact the named control is not directly in the main View, so Caliburn's search to find an element in the View with that specific name may not be working as expected.

This answer suggests writing the binding explicitly, like this :

<Button cal:Message.Attach="ConfirmDelete" />
Chrismatory answered 13/4, 2015 at 16:0 Comment(5)
Thanks Rachel for your help. So i follow your advice and remove ConfirmDelete from ClientesViewModel and put it into ModalViewModel. That seems logical. Never the less the problem persist: ModalView seems to be disconnected from its viewmodel and when i click the button the method ConfirmDelete is never called. What i'm i missing here? Thanks!Acrid
@Acrid My best guess is it's something in how your ModalContentPresenter UserControl displays the custom ModelContent property. Can you share the relevant XAML for that control?Chrismatory
I also thought that was my problem, but it seems not. Please see my Edit 2 in wich i change modalcontenPresenter for a simple contentpresenter. Still my method is never called. Also i re-arange viewmodels according to your advice. Thanks!Acrid
@Acrid See the update at the bottom of my answer :)Chrismatory
Wow! That did it. Incredible. The only problem now is that TryClose() didn't work, i suppose it has to be with the way that the view is created. Now i only need to find a way to close the TestView created. Thanks!Acrid
C
0

I think the problem is that the naming conventions aren't working since the view was not set using Caliburn.Micro's ViewLocator .

Try explicitly setting the model .

<local:ModalContentPresenter cal:Bind.Model="{Binding}" 
          DataContext="{Binding}" IsModal="{Binding DeleteConfirmVisible}" Grid.ColumnSpan="5" Grid.RowSpan="4" ModalContent="{Binding Path=ModalDelete}">
    <Grid>        
        <Button x:Name="ShowDeleteConfirm" Margin="5" Grid.Column="2" Content="Delete Record"/>        
    </Grid>
</local:ModalContentPresenter>
Cloutier answered 13/4, 2015 at 18:44 Comment(1)
Naming convention is the problem indead. But this code didn't help for that. Rachel code did the trick. Thanks!Acrid

© 2022 - 2024 — McMap. All rights reserved.