首页 > 解决方案 > WPF 用户控件的绑定组在使用数据上下文或如何将验证错误转发到用户控件时不起作用

问题描述

情况:我在 WPF MVVM 应用程序中有一个自定义编辑对话框,其中包含一个选项卡控件和用于确定/取消的按钮。编辑对话框的视图模型由其他用于个人数据、地址数据等的视图模型组成。每个选项卡项只加载一个用户控件,该控件应显示来自相应视图模型的数据。在下文中,我将展示我的实际代码的精简版本,它只关注人员数据(名字和姓氏)。

/// ViewModelBase just provides INotifyPropertyChanged logic
public class PersonViewModel : ViewModelBase
{
    private Person model;

    public string FirstName
    {
        get => model.FirstName;
        set
        {
            if (!Equals(model.FirstName, value))
            {
                model.FirstName = value;
                OnPropertyChanged();
            }
        }
    }

    public string LastName
    {
        get => model.LastName;
        set
        {
            if (!Equals(model.LastName, value))
            {
                model.LastName = value;
                OnPropertyChanged();
            }
        }
    }

    // ...
}

个人控制.xaml:

<UserControl x:Class="MvvmDemo.Views.PersonControl"
             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:vm="clr-namespace:MvvmDemo.ViewModels"
             mc:Ignorable="d" 
             d:DesignHeight="250" d:DesignWidth="400"
             d:DataContext="{d:DesignInstance Type=vm:PersonViewModel}">
    <!-- Variant A: using dependency properties
    <Grid DataContext="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:PersonControl}}}">
    -->
    <!-- Variant B: using parent data context -->
    <Grid>
        <Grid.Resources>
            <Style TargetType="{x:Type Label}">
                <Setter Property="HorizontalAlignment" Value="Right"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
            </Style>

            <Style TargetType="{x:Type TextBox}">
                <Setter Property="Margin" Value="5"/>
            </Style>
        </Grid.Resources>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <Label Grid.Row="0" Grid.Column="0" Content="_First Name:" Target="{Binding ElementName=txtFirstName}"/>
        <TextBox Grid.Row="0" Grid.Column="1" x:Name="txtFirstName" Text="{Binding FirstName}"/>

        <Label Grid.Row="1" Grid.Column="0" Content="_Last Name:" Target="{Binding ElementName=txtLastName}"/>
        <TextBox Grid.Row="1" Grid.Column="1" x:Name="txtLastName" Text="{Binding LastName}"/>
    </Grid>
</UserControl>

PersonControl.xaml.cs:

public partial class PersonControl : UserControl
{
    #region Properties

    public static readonly DependencyProperty FirstNameProperty =
        DependencyProperty.Register("FirstName", typeof(string), typeof(PersonControl),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    public static readonly DependencyProperty LastNameProperty =
        DependencyProperty.Register("LastName", typeof(string), typeof(PersonControl),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    public string FirstName
    {
        get => (string)GetValue(FirstNameProperty);
        set => SetValue(FirstNameProperty, value);
    }

    public string LastName
    {
        get => (string)GetValue(LastNameProperty);
        set => SetValue(LastNameProperty, value);
    }

    // ...

    #endregion

    public PersonControl()
    {
        InitializeComponent();
    }
}

EditPersonView.xaml:

<Window x:Class="MvvmDemo.Views.EditPersonView"
        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:views="clr-namespace:MvvmDemo.Views"
        xmlns:viewmodels="clr-namespace:MvvmDemo.ViewModels"
        mc:Ignorable="d"
        Title="Edit Person" Height="450" Width="600"
        WindowStartupLocation="CenterOwner" WindowStyle="ToolWindow" ShowInTaskbar="False" Loaded="HandleWindowLoaded"
        d:DataContext="{d:DesignInstance Type=viewmodels:EditPersonViewModel}">
    <Window.BindingGroup>
        <BindingGroup Name="editBindingGroup">
        </BindingGroup>
    </Window.BindingGroup>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <TabControl Grid.Row="0">
            <!-- Variant A: dependency property for each parameter -->
            <TabItem Name="tabGeneral" Header="_General">
                <views:PersonControl FirstName="{Binding Person.FirstName}"
                                     LastName="{Binding Person.LastName}"/>
            </TabItem>
            <!-- Variant B: set data context for control -->
            <TabItem Name="tabGeneral" Header="_General">
                <views:PersonControl DataContext="{Binding Person, BindingGroupName=editBindingGroup}"/>
            </TabItem>
        </TabControl>

        <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
            <Button Name="btnOk" Content="_OK" Width="60" Margin="10,10,0,10" IsDefault="True" Click="OkClicked"/>
            <Button Name="btnCancel" Content="_Cancel" Width="60" Margin="10" IsCancel="True" Click="CancelClicked"/>
        </StackPanel>
    </Grid>
</Window>

EditPersonView.xaml.cs

public partial class EditPersonView : Window
{
    public EditPersonView()
    {
        InitializeComponent();
    }

    private void OkClicked(object sender, RoutedEventArgs e)
    {
        e.Handled = true;

        if (BindingGroup.CommitEdit())
        {
            DialogResult = true;
            Close();
        }
    }

    private void CancelClicked(object sender, RoutedEventArgs e)
    {
        e.Handled = true;

        BindingGroup.CancelEdit();

        DialogResult = false;
        Close();
    }
}

起初,我使用用户控件的依赖属性来设置值。例如,PersonControl将具有 DP FirstNameLastName并且这些将通过编辑对话框中的正常数据绑定设置(请参阅变体 A 的 XAML)。但是,当向视图模型添加数据验证时,整个用户控件将由红色边框标记,而不是受影响的文本框(考虑到绑定的内容,这是有道理的)。

因为我不知道如何将验证错误转发到用户控件中的正确文本框,所以我决定切换到变体 B 并将DataContext控件的设置为相应的子视图模型。但是现在绑定组成员身份丢失了。如果我编辑文本框,源会立即更新,而不仅仅是在按下 OK 按钮时。

在调试时,我注意到如果我设置数据上下文,绑定不会添加到绑定组的项目列表中。仅当我使用依赖项属性时才会出现人员视图模型。

因此,要解决这个问题,我必须找到一种方法:a)如果我使用 DP,将验证错误转发到正确的用户控件文本框,或者 b)在设置控件的数据上下文时保持绑定组完整的方法。我真的走到了死胡同,我希望我能在这里找到一些帮助。

BindingGroup.CommitEdit()我还尝试“嵌套”绑定组,即人员控件提供提交/取消功能,但如果其中一个调用由于验证错误而失败,则可能会导致部分更新。必须有一种方法可以将它放在同一个绑定组中,并让框架的逻辑处理这个问题。

标签: c#wpfxamlmvvm

解决方案


推荐阅读