65.9K
CodeProject 正在变化。 阅读更多。
Home

MVVM:事件路由到子 ViewModel

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (9投票s)

2014年5月26日

CPOL

13分钟阅读

viewsIcon

42885

downloadIcon

877

当有子 ViewModel 的集合时,如何将事件路由到子 ViewModel。

引言

目前,MVVM 模式是 WPF 应用程序架构设计中最推荐的模式。

MVVM 原理基于以下组件

通常,我们以一种方式设计我们的应用程序,即一个 View 对应一个 ViewModel。ViewModel 负责为其 View 提供/更新数据和处理事件。从上面的图可以清楚地看到,对于 Button、ContextMenu 等控件,ViewModel 中定义了事件处理程序,例如 OnButtonClickEvent。因此,当发生点击等事件时,注册/指定的事件处理程序将被调用。

注意: View 和 ViewModel 的绑定是通过 DataContext 或 Content [如果 XAML 中使用的元素是 ContentControl] 来完成的。为了开发这个示例程序,我选择了 Caliburn.Micro 框架。从这里下载 Caliburn.micro。

背景/目标

我们可能需要一个 View,它可以附加到 ViewModel 集合中的某个 ViewModel 实例 [相同类型 ViewModel 的多个实例]。

数据绑定和事件路由会按预期工作吗?

这是场景的理想图示。

图 1:事件路由到 ViewModel

所以,这就是我们期望应用程序的工作方式,即在选择子视图索引后,子视图将绑定到相应的子 ViewModel。显示的数据和子视图的事件路由将由当前选定的子 ViewModel 处理。

为了验证上述场景,我使用 Caliburn 框架开发了一个 WPF 应用程序,该应用程序代表学生数据。根据所选学生,应用程序将调用注册的电话号码。在完成这个 POC [概念验证] 后,我得到了以下观察结果:

  • 选择所需的学生后,学生视图显示正确的信息,如学生姓名和学生号码。

注意:默认情况下,第一个选中的索引是“学生 A”,之后用户可以将其更改为“B”、“C”或“A”。

学生 A:点击“联系”按钮时,会弹出一个消息框,显示“正在联系学生 A:XXXXXXXXX”。

学生 B/C:点击“联系”时,会显示与“学生 A”相同的信息,即“正在联系学生 A:XXXXXXXXX”。对于学生 C,观察结果也相同。

注意:如果代码中将默认学生从 A 更改为 B 或 C,则点击任何学生的“联系”按钮时,消息框都会显示默认学生的信息。

Using the Code

这是承载 StudentInfoViewMainWindow 视图。

<Window x:Class="ContactStudent.Views.MainWindowView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro"
        xmlns:nsVm="clr-namespace:ContactStudent.ViewModels"
        xmlns:nsVi="clr-namespace:ContactStudent.Views"
        Title="Student Details" Height="400" Width="488" ResizeMode="NoResize" Background="DarkGray">
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="90"/>
            <RowDefinition Height="302"/>
            <RowDefinition Height="3*" />
        </Grid.RowDefinitions>
        <Grid Grid.Row="0" Background="DarkGray">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
                <!--<Three Students i.e. A B C>-->
            <Button Name="Student_A" Grid.Column="0" Content="A" Width="110" Margin="22,22,22,28" 
                        ToolTip="Student A Information" 
                        cal:Message.Attach="[Event Click]=[Action DisplayStudentDetails('A')]"/>
            <Button Name="Student_B" Grid.Column="1" Content="B" Width="110" Margin="22,22,22,28"
                    ToolTip="Student B Information" 
                    cal:Message.Attach="[Event Click]=[Action DisplayStudentDetails('B')]"/>
            <Button Name="Student_C" Grid.Column="3" Content="C" Width="110" Margin="22,22,22,28"
                    ToolTip="Student C Information" 
                    cal:Message.Attach="[Event Click]=[Action DisplayStudentDetails('C')]"/>
        </Grid>
        <nsVi:StudentInfoView Grid.Row="1" DataContext="{Binding Student}" Background="DarkGray" HorizontalAlignment="Left"/>

    </Grid>
</Window>

上面代码片段中突出显示的 [粗体标记] 部分显示了 StudentInfoView 及其 DataContext 绑定。它也可以如下所示:

<ContentControl Grid.Row="1" Content="{Binding Student}" Background="DarkGray" HorizontalAlignment="Left"/>

StudentInfoView 按钮 Message.Attach 属性已用粗体格式突出显示。

<UserControl x:Class="ContactStudent.Views.StudentInfoView"
             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:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro"
             xmlns:local="clr-namespace:ContactStudent"
             mc:Ignorable="d" 
             Height="185" Width="302" Background="DarkGray">
    <Grid>
        <TextBlock Text="STUDENT'S DETAIL" TextAlignment="Center" FontWeight="Bold" Margin="64,0,96,165" />
        <TextBlock Name="Student_Name" Text="Student Name" FontWeight="Bold"                             Margin="-1,37,206,128" TextAlignment="Center"/>
        <TextBlock Name="Student" Text="{Binding StudentName}" FontWeight="Bold"                         Background="LightGray" Margin="123,35,33,128" TextAlignment="Left"/>
        <TextBlock Name="Contact_Number" Text="Phone Number" FontWeight="Bold"                           Margin="0,69,206,97" TextAlignment="Center"/>
        <TextBlock Name="Number" Text="{Binding PhoneNumber}" FontWeight="Bold"                         Background="LightGray" Margin="123,69,33,97" />
        <Button Name="Contact" Content="Contact" Margin="149,108,90,51" BorderBrush="Black" 
         cal:Message.Attach="[Event Click]=[Action OnContactClick($source, $eventArgs)]"/>      
    </Grid>
</UserControl>

MainWindowViewModel:它描绘了在 DisplayStudentDetails(); 下如何进行 StudentViewModel 索引选择,请参考以下代码。

    using Caliburn.Micro;
    public class MainWindowViewModel : PropertyChangedBase
    {
        #region Data Member
        private static int              m_nCurrentStudentIndex = 0; //Current selected student index
        private StudentInfoViewModel[]  m_strStudentDetails; //Collection of students
        #endregion Data Member

        #region Constructor
        public MainWindowViewModel()
        {
            //allocate the memory for the Student A,B and C
            m_strStudentDetails = new StudentInfoViewModel[3]; 
            InitializeStudent();//Initialize StudentInfoViewModel instances to hold the information
            NotifyOfPropertyChange(() => Student);
            
        }
        #endregion Constructor

        #region Private Methods
        /// <summary>
        /// Initialization
        /// </summary>
        private void InitializeStudent()
        {
            int nIndex = 0;
            m_strStudentDetails[nIndex] = new StudentInfoViewModel("A", 1122334456);
            nIndex++;
            m_strStudentDetails[nIndex] = new StudentInfoViewModel("B", 1111111111);
            nIndex++;
            m_strStudentDetails[nIndex] = new StudentInfoViewModel("C", 1111111122);     
            
        }
        #endregion Private Methods

        #region Event Handler
        /// <summary>
        /// Click Event Handler for the Button A,B and C
        /// </summary>
        /// <param name="strStudent">A/B/C</param>
        public void DisplayStudentDetails(String strStudent)
        {
            switch (strStudent)
            {
                case "A":
                    m_nCurrentStudentIndex = 0;
                    break;
                case "B":
                    m_nCurrentStudentIndex = 1;
                    break;
                case "C":
                    m_nCurrentStudentIndex = 2;
                    break;
            }
            NotifyOfPropertyChange(() => Student);
        }
        #endregion Event Handler

        #region Properties
        /// <summary>
        /// Get Current selected Student
        /// </summary>
        public StudentInfoViewModel Student
        {
            get
            {
                return m_strStudentDetails[m_nCurrentStudentIndex];
            }
        }
        #endregion Properties
    }

DisplayStudentDetails() 是处理 MainWindowView 中按钮的点击事件的事件处理程序。此处理程序根据单击的按钮来确定 StudentInfoViewModel 的索引。

需要注意的一点是,学生视图显示正确的信息,如姓名和电话号码;这意味着附加的 ViewModel 是根据所选学生设置的。换句话说,DataContext 正在更新。

那么,为什么“联系”按钮的点击事件没有路由到附加的 ViewModel?为什么它总是只路由到第一个附加的 ViewModel?Message.Attach 属性是否在 DataContext 更改时更新?

任何初学者,如果尝试将单个 View 附加到 ViewModel 集合中的任何实例,都可能遇到与将事件路由到正确的 ChildViewModel 相关的问题。

问题

当一个子视图放置在一个主视图内部,并且子视图通过 DataContext 或 Content 绑定到其 ViewModel 时。

主视图可以通过选择索引来更新子视图,这又会在运行时通过 DataContext/Content 更新子 ViewModel 实例;在这种情况下,子视图的 DataContext/Content 属性的更改不会更新 Message.Attach 属性。

换句话说,子视图的动作处理程序的 Target 不会随着主视图内部的选择更新而更新。

解决方案

我建议了几种/多种方法来解决这个问题。每种方法都有其优缺点。

解决方案 1:维护 View 与 ViewModel 的一对一映射

此解决方案涉及在 View 和 ViewModel 之间维护一对一的映射,如下面的图所示。

图 2:View 与 ViewModel 之间的一对一映射

在附加文档中定义了放置多个 ContentControl 并将它们绑定到它们的 ViewModel 实例。

这种方法对于小型应用程序非常简单易实现。它基本上是关于维护每个 ViewModel 的视图。

根据当前选定的索引,View 将对用户可见,其他视图实例将不可见;与所选视图相关的 ViewModel 也将处于活动状态。

对于我只针对三个学生的应用,我需要更新 MainWindow View 以容纳三个 StudentView 的三个 ContentControl 实例。根据当前选定的索引,相应的 ContentControl 将可见。

MainWindowView

    <Grid Grid.Row="0" Background="DarkGray">
       <Grid.ColumnDefinitions>
          <ColumnDefinition Width="*"/>
          <ColumnDefinition Width="*"/>
          <ColumnDefinition Width="*"/>
       </Grid.ColumnDefinitions>
       <!--<Three Students i.e. A B C>-->
       <Button Name="Student_A" Grid.Column="0" Content="A" Width="110" Margin="22,22,22,28" 
                        ToolTip="Student A Information" 
                        cal:Message.Attach="[Event Click]=[Action DisplayStudentDetails('A')]"/>
       <Button Name="Student_B" Grid.Column="1" Content="B" Width="110" Margin="22,22,22,28"
                    ToolTip="Student B Information" 
                    cal:Message.Attach="[Event Click]=[Action DisplayStudentDetails('B')]"/>
       <Button Name="Student_C" Grid.Column="3" Content="C" Width="110" Margin="22,22,22,28"
                    ToolTip="Student C Information" 
                    cal:Message.Attach="[Event Click]=[Action DisplayStudentDetails('C')]"/>
        </Grid>
    <ContentControl Grid.Row="1" Content="{Binding StudentArr[0]}" Background="DarkGray"
                    Visibility="{Binding IsVisible[0]}"/>
    <ContentControl Grid.Row="1" Content="{Binding StudentArr[1]}" Background="DarkGray"
                    Visibility="{Binding IsVisible[1]}"/>
    <ContentControl Grid.Row="1" Content="{Binding StudentArr[2]}" Background="DarkGray"
                    Visibility="{Binding IsVisible[2]}"/> 

突出显示的 [粗体标记] 行是 MainWindowView 的修改,用于承载三个 StudentInfoView。每个 ContentControl 都有自己的 ContentVisibility 属性。

注意: Content 属性是一个 Object,它包含控件的内容。因为 Content 属性的类型是 Object,所以对可以放入 ContentControl 的内容没有限制。

MainWindowViewModel

ViewModel 也需要修改,以提供每个 ContentControl 的 Content 和 Visibility,以放置相应的 StudentInfoViewModel

public class MainWindowViewModel : PropertyChangedBase
    {
        #region Data Member
        private static int m_nCurrentStudentIndex = 0; //Current selected student index
        private StudentInfoViewModel[] m_strStudentDetails; //Collection of students
        private Visibility[] m_IsSelected; //Collection of Visibility Property for ContentControl element

        #endregion Data Member

        #region Constructor
        public MainWindowViewModel()
        {
            m_strStudentDetails = new StudentInfoViewModel[3]; // A,B and C
            m_IsSelected = new Visibility[3];
            InitializeStudent();//Initialize StudentInfoViewModel instances to hold the information
            vUpdateVisibility();  //Initialize the visibilty property                      
        }
        #endregion Constructor


        #region Private Methods
        /// <summary>
        /// Initialization
        /// </summary>
        private void InitializeStudent()
        {
            int nIndex = 0;
            m_strStudentDetails[nIndex] = new StudentInfoViewModel("A", 1122334456);
            nIndex++;
            m_strStudentDetails[nIndex] = new StudentInfoViewModel("B", 1111111111);
            nIndex++;
            m_strStudentDetails[nIndex] = new StudentInfoViewModel("C", 1111111122);

        }
        #endregion Private Methods


        private void vUpdateVisibility()
        {
            switch (m_nCurrentStudentIndex)
            {
                case 0:
                    m_IsSelected[0] = Visibility.Visible;
                    m_IsSelected[1] = Visibility.Hidden;
                    m_IsSelected[2] = Visibility.Hidden;
                    break;
                case 1:
                    m_IsSelected[1] = Visibility.Visible;
                    m_IsSelected[0] = Visibility.Hidden;
                    m_IsSelected[2] = Visibility.Hidden;
                    break;
                case 2:
                    m_IsSelected[2] = Visibility.Visible;
                    m_IsSelected[0] = Visibility.Hidden;
                    m_IsSelected[1] = Visibility.Hidden;
                    break;
            }
            NotifyOfPropertyChange(() => IsVisible);
        }

        #region Event Handler
        /// <summary>
        /// Click Event Handler for the Button A,B and C
        /// </summary>
        /// <param name="strStudent">A/B/C</param>
        public void DisplayStudentDetails(String strStudent)
        {
            switch (strStudent)
            {
                case "A":
                    m_nCurrentStudentIndex = 0;
                    break;
                case "B":
                    m_nCurrentStudentIndex = 1;
                    break;
                case "C":
                    m_nCurrentStudentIndex = 2;
                    break;
            }
            vUpdateVisibility();
            NotifyOfPropertyChange(() => StudentArr);
        }
        #endregion Event Handler
        /// <summary>
        /// Visibility of the ContentControl
        /// </summary>
        public Visibility[] IsVisible
        {
            get
            {
                return m_IsSelected;
            }
        }
        /// <summary>
        /// Content property value for ContentControl
        /// </summary>
        public StudentInfoViewModel[] StudentArr
        {
            get
            {
                return m_strStudentDetails;
            }
        }

    }   
    
} 

从 ViewModel 的实现可以清楚地理解,我已经维护了 VisibilityStudentInfoViewModel 的数组。DisplayStudentDetails() 方法决定了当前选中的 ContentControl。根据 m_nCurrentStudentIndex vUpdateVisibility () 将当前选定的设置为可见,其他设置为隐藏。

结论

放置多个 ContentControl 实例并更改它们的可见性,其性能优势在于仅通过强制重建视图来更新控件布局。换句话说,ContentControl 将始终可用在 GUI 中,并且在绑定属性更新时易于引用。

在这种情况下使用一对一映射是可以的,因为我们只处理 3 个学生,试想一下需要数千甚至更多实例的情况。MainWindowView.xaml 的大小会有多大?

其次,我们使用的是 ContentControl 框架元素。当 Content 直到运行时才知道时,ContentControl 是适用的。Content 可以是字符串或任何 Object。但在此场景中,我们知道 View 和 Object,所以我们应该避免使用 ContentControl,因为它可以用其他框架元素替代。

第三,对于包含更多控件(如按钮或更多 UI 元素)的子视图,加载和卸载时间会增加。

它还会影响应用程序的大小。

解决方案 2:在 ViewModel 中访问 UI 元素并更新事件处理程序

此解决方案是关于在 ViewModel 中访问 UI 元素,并根据所选索引分配相关的事件处理程序,例如,我们可以在 ViewModel 中获取按钮句柄并更改事件处理程序。

以下部分解释了如何在 ViewModel 中订阅 UI 元素的事件处理程序。事件处理程序可以根据当前选定的 ViewModel 更新。通过遵循此方法,UI 控件事件可以路由到可用 ViewModel 集合中的目标 ViewModel。

实现步骤

a. 在 ViewModel 中获取 UI 元素

要在 ViewModel 中获取 UI 元素,必须处理 UserControl 的“Loaded”事件,即 StudentInfoView

<UserControl x:Class="ContactStudent.Views.StudentInfoView"
             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:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro"
             xmlns:local="clr-namespace:ContactStudent"
             mc:Ignorable="d" 
             Height="185" Width="302" Background="DarkGray" cal:Message.Attach="[Event Loaded] = [Action vOnLoaded($Source)]" > 

注意: 事件处理程序使用 Caliburn 的 Message.Attach 属性定义。另外,请移除在 StudentInfoView.xaml 中为“联系”按钮附加的点击事件。

StudentInfoViewModel

public static Button ContactBtn
{
    get;
    set;
}   

//Handles the Loaded event for the view and assign the handler for the Click Event
public void vOnLoaded(object sender)
{
    ContactBtn = ((StudentInfoView)sender).Contact;
    ContactBtn.Click += OnContactClick;   }

突出显示的部分是控件的 Name 属性的值,在本例中,Button 是控件,ContactName 属性的值。

b. 更新控件事件(例如 Click)的事件处理程序

以学生为例,“联系”是一个按钮,每次选择学生时都需要更新其 Click 事件,以便 Click 事件可以路由到目标 ViewModel。这种切换是在 MainWindowViewModel 中定义的,因为它充当 StudentInfoViewModel 的控制器。

/// <summary>
/// Click Event Handler for the Button A,B and C
/// <param name="strStudent">A/B/C</param>
public void DisplayStudentDetails(String strStudent)
{
   StudentInfoViewModel.ContactBtn.Click -= Student.OnContactClick;
   switch (strStudent)
   {
       case "A":
           m_nCurrentStudentIndex = 0;
           break;
       case "B":
            m_nCurrentStudentIndex = 1;
            break;
       case "C":
            m_nCurrentStudentIndex = 2;
            break;
    }
    StudentInfoViewModel.ContactBtn.Click += Student.OnContactClick;
    NotifyOfPropertyChange(() => Student);
 }

突出显示的部分是 DisplayStudentDetails() 方法的附加内容。正如我之前提到的,这是根据 GUI 上的选择来识别当前选定的 StudentInfoViewModel 的方法。通常,这种路由可以定义在负责处理选择事件更改的事件处理程序中。

结论

此实现涉及在 ViewModel 中访问 UI 元素,但这会强制 View 和 ViewModel 紧密耦合。

对控件的任何更改都会强制在 ViewModel 中实现更改。这种实现对于小型应用程序是没问题的,意味着当 UI 中没有那么多需要以同样方式处理的控件时。

随着控件数量的增加,如果 ViewModel 实例的数量也很多,那么将需要更多地关注处理程序的注册和注销。如果这种情况没有得到妥善处理,可能会导致应用程序崩溃。

您需要非常小心需要内部 ViewModel 访问的控件数量,并且您是否准备好违背 MVVM 原则。

解决方案 3:使用 DependencyProperty

定义的问题也可以使用 DependencyProperty 来解决,即可以通过样式、数据绑定、动画和继承等方法设置的属性。

依赖属性是派生自 DependencyObject 的类的属性,它们很特别,因为它们不是简单地使用后端字段来存储其值,而是使用 DependencyObject 的一些辅助方法。

依赖属性是一个公共的静态只读字段,必须先注册。注册后,此静态属性用于在内部存储系统中获取和设置值。

以下部分解释了使用 DependencyProperty 解决问题的步骤。

实现步骤

a. 在 CustomAttachedProperties 类中定义 DependencyProperty

public static class CustomAttachedProperties
    {
        public static readonly DependencyProperty AttachExProperty =
            DependencyProperty.RegisterAttached(
                "AttachEx",
                typeof(string),
                typeof(CustomAttachedProperties),
                new PropertyMetadata(null, OnAttachExChanged)
                );

        public static void SetAttachEx(DependencyObject d, string attachText)
        {
            d.SetValue(AttachExProperty, attachText);
        }

        public static string GetAttachEx(DependencyObject d)
        {
            return d.GetValue(AttachExProperty) as string;
        }

        private static void OnAttachExChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = d as FrameworkElement;

            if (control == null)
            {
                return;
            }


            control.DataContextChanged += (sender, args) =>
            {
                //Need to clear AttachProperty value as Caliburn forces the MessageTrigger only when         AttachProperty 
                //has different value then previous one
                control.ClearValue(Message.AttachProperty);
                control.SetValue(Message.AttachProperty, e.NewValue);
            };
        }

    }

在上面的代码片段中,我们使用了 RegisterAttached 来注册属性,即 AttachEx,以及 PropertyMetaData 和其 PropertyChangedCallback,即 OnAttachExChanged 实现的引用。

b.StudentInfoView 中使用依赖属性为其控件定义事件。

<Button Name="Conatct" Content="Contact"local:CustomAttachedProperties.AttachEx="[Event Click]=[Action OnContactClick($source,$eventargs)]" Margin="149,108,90,51" />       

突出显示的节显示了如何在 View(即 StudentInfoView)中使用 DependencyProperty。代替使用 Caliburn 的 Message.Attach 属性,使用 DependencyProperty,即 AttachEx,来订阅 DataContextChanged 事件。因此,每当 PropertyMetadata 发生变化时,它就会调用 OnAttachExChanged()。此方法内部处理 DataContextChanged 事件,在该事件中清理控件的 Message.AttachProperty 并设置新值。

结论

DependencyProperty 用于启用声明式代码来修改对象的属性,通过提供更强大的数据变更通知系统来减少数据需求。当应用程序需要某种动画或需要允许在 Style 控件中设置属性时,DependencyProperty 非常有用。

缺点

  • DependencyProperties 旨在供 WPF 的绑定系统使用,而 WPF 的绑定系统是将 UI 层与数据层联系起来的。它们应该保留在 UI 层,而不是数据层(ViewModels)中。
  • DependencyProperties 主要适用于 VisualElements 层面,所以为我们的每个业务需求创建大量的 DP 可能不是个好主意。此外,DP 的成本比 INotifyPropertyChanged 要高。

解决方案 4:使用 Caliburn 的 View.Model 属性

此解决方案仅与 Caliburn 的属性相关,而我们中的许多人刚开始接触 C# 和 MVVM 实现的各种框架时可能并不知道这一点。

View.Model 属性是 Caliburn 为我提供的发现。在研究了所有方法之后,我才了解到这个属性。

更准确地说,在 WPF 语言中,View.Model 是 Caliburn.Micro 提供的 DependencyProperty。它为我们提供了一种在 ContentControl 中托管内容、将其绑定到 View.Model 提供的模型以及根据所选模型更新视图的方法。

实现步骤

我首先要提到的是,这在控制 ViewModel 选择的用户控件中是一个非常简单的一行更改。

只需像下面这样更新 MainWindowView,突出显示的属性是解决问题的关键。J

<nsVi:StudentInfoView Grid.Row="1" cal:View.Model="{Binding Student}" Background="DarkGray"/>

 <ContentControl Grid.Row="1" cal:View.Model="{Binding Student}" Background="DarkGray"/> 

View.Model

此属性将视图绑定到指定的模型。在此示例中,视图或 ContentControl 绑定到“Student”,即当前 DataContext。它不仅处理 DataContext 的更改,还考虑了子视图 Message.Attach 属性的更新。

由于 Message.Attach 属性类似于为某些事件定义委托,而 view.model 通过将视图附加到当前 DataContext 或告诉视图这是数据和事件处理的 ViewModel/数据层来处理委托。

因此,我们讨论了四种解决方案及其优缺点,我想总结一下这些方法。

总结/结论

在 StudentInfo 视图的事件处理程序中使用 Caliburn micro 的 Message.Attach 属性定义 [使用 Message.Attach 不会发生绑定]。

所有这些只是为按钮定义的事件(如点击)设置了一个 EventTrigger,其 Action 类型为 Caliburn.Micro.ActionMessage。这对于每个创建的视图只发生一次!因此,当 DataTemplate 实例被宿主视图回收时,尽管 DataContext/Content 和所有相关绑定都已更新(StudentInfoView 显示正确的信息,如学生姓名和号码),但与此视图关联的 ActionMessage 从未设置为目标,即新的 ViewModel。

为了克服这个问题,我提出了四种解决方案,其中我更喜欢解决方案 4,因为它使用 Caliburn 的属性本身来解决问题,而且只需进行最小的更改。通过将 WPF 的 DataContext/Content 属性替换为解决方案 4,我们的 DataContext 现在变成了绑定到一个 DependencyProperty(Caliburn.Micro.View.Model),当模板的 DataContext 更改时,此 DependencyProperty 会更新。这会导致 Caliburn.Micro 实例化一个新视图(View.Model 依赖属性的更改处理程序调用 ViewLocator,如果不存在则实例化一个新视图),其中 ActionMessage 被正确绑定。

尽管使用用户定义的 DependencyProperty、在 ViewModel 中获取 UI 元素等其他方法有助于解决问题,但它们也有其优缺点。所有这些方法都可以用于有限数量的控件,因为随着控件数量的增加,它将影响维护。其次,这些方法将违背 MVVM 原则。

参考文献

http://maonet.wordpress.com/2010/09/24/nested-viewusercontrol-binding-in-caliburn/

http://blog.martindoms.com/2013/01/15/caliburn-micro-datatemplates-actionmessages-and-virtualized-itemscontrols/

http://msdn.microsoft.com/en-us/library/ms752914(v=vs.110).aspx
© . All rights reserved.