Shared viewmodel between two Views in WPF MVVM pat

2019-06-02 15:39发布

问题:

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:

回答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" />


回答2:

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>