试图理解 DependencyProperty

Posted

技术标签:

【中文标题】试图理解 DependencyProperty【英文标题】:Trying to understand of DependencyProperty 【发布时间】:2011-11-27 08:47:54 【问题描述】:

对 WPF 不熟悉,它显然具有惊人的更改、绑定、启用和以其他方式操作的能力。我正在尝试对正在发生的事情进行心理概述,并希望有些人可以确认或纠正我的读数。

在 WPF 之前,您有委托和事件。您可以有十几个控件都在监听(通过注册到事件),因此当事件触发时,所有其他控件将自动收到通知,并且可以按照它们的编码方式进行操作。比如……

从代码后面,你会做类似的事情

GotFocus += MyMethodToDoSomething;

然后,签名方法

private void MyMethodToDoSomething(object sender, RoutedEventArgs e)

  .. do whatever

此外,通过使用标准的 getter/setter,setter 可以在每次有人尝试获取或设置值时调用自己类中的方法来做某事

private int someValue;
public int SomeValue

   get  this.DoSomeOtherThing();
         return someValue;
       

   set  this.DoAnotherThing();
        someValue = value;

现在,有依赖属性和单向/双向绑定。我了解(我认为)模拟更多只读操作的一种方法。

无论如何,通过两种方式绑定,依赖项会自动通知任何人“依赖”源或目标中的更改,而无需显式检查是否有订阅事件,框架会自动处理更改的通知到相应的控件(目标或源)。

所以,让我用一个旧的“添加/编辑保存/取消”维护表单完成这个场景。 在较旧的框架中,如果有人单击添加或编辑按钮,则所有数据输入字段都将变为“启用”,新记录的空白数据或编辑现有数据。同时,添加/编辑按钮将被禁用,但保存/取消按钮现在将被启用。

同样,当通过保存/取消完成时,它会禁用所有输入字段、保存/取消并重新启用添加/编辑按钮。

我不太明白在这种依赖属性场景下如何处理这种类型的场景(还),但我关闭了吗?我也知道你几乎可以绑定到任何东西,包括配色方案、显示/隐藏、字体等……但我正在尝试真正掌握这些东西。

谢谢。

【问题讨论】:

因为WPF是一个很大的话题,而且你的问题比较广泛,很难回答。我很乐意为您提供我最喜欢的 WPF 资源的链接。例如,您是否阅读过 Model-View-ViewModel 模式?这是一个很好的演示:blog.lab49.com/archives/2650 您的问题似乎是一篇关于依赖属性的文章。我什至没有读过它。 @Corey Kosak,如果您将您的评论作为答案发表,我会检查解决方案,因为它提供了最好的分步理解,而无需购买一本书。 这个问题展示了研究,所以它绝对值得被问到。太宽泛了,呵呵。为了确保我的理解清楚 - 你的问题是否归结为:“什么是依赖属性?我将如何在这种情况下使用它们:一个以只读方式启动并且可以编辑的表单?”看来您得到的答案(以及您接受的答案)只是进一步阅读。 添加了一个答案,试图阐明如何使用数据绑定,并解释为什么您所描述的场景不需要依赖属性。 【参考方案1】:

getter/setter 是常规 C# 属性的一个特性。它不是 WPF 独有的。

这个单向/双向的东西是关于 WPF 数据绑定,它不需要你创建依赖属性 - 只是为了使用它们。

依赖属性内置在控件本身中。它们允许您在将控件实例添加到表单时直接引用这些属性。它们让您的自定义控件感觉更“原生”。

通常它们用于实现可以使用数据绑定的属性。在您的应用程序中,您大多只是使用数据绑定,而不是为它实现新的钩子。

...如果有人单击添加或编辑按钮,所有数据输入字段都将变为“启用”,新记录的空白数据或编辑现有数据。同时,添加/编辑按钮将被禁用,但保存/取消按钮现在将被启用。

同样,当通过保存/取消完成时,它会禁用所有输入字段、保存/取消并重新启用添加/编辑按钮。

我会完成你想要完成的事情:

视图模型 视图上的数据绑定到该视图模型 在该视图模型上公开 ICommand(用于按钮) 视图模型上的 INotifyPropertyChanged(适用于所有属性)

不需要为此场景创建新的依赖属性。您只需使用现有的进行数据绑定。

这是使用数据绑定和 MVVM 样式进行 WPF 的代码示例/教程。

设置项目

我在新建项目向导中创建了一个 WPF 应用程序,并将其命名为 MyProject

我设置了我的项目名称和命名空间以匹配普遍接受的事物方案。您应该在解决方案资源管理器 -> 项目 -> 右键单击​​ -> 属性中设置这些属性。

我还有一个我喜欢用于 WPF 项目的自定义文件夹方案:

出于组织目的,我将视图保存在其自己的“视图”文件夹中。这也反映在命名空间中,因为您的命名空间应该与您的文件夹匹配 (namespace MyCompany.MyProject.View)。

我还编辑了 AssemblyInfo.cs,并清理了我的程序集引用和应用程序配置,但这只是一些乏味的事情,我将留给读者作为练习:)

创建视图

从设计师开始,让一切看起来都很好。不要在后面添加任何代码,或者做任何其他工作。只需在设计器中玩耍,直到看起来正确(尤其是在调整大小时)。这是我最终得到的结果:

查看/EntryView.xaml:

<Window x:Class="MyCompany.MyProject.View.EntryView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Entry View" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid Grid.Row="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <TextBox Text="Test 1" Grid.Row="0" />
            <TextBox Text="Test 2" Grid.Row="1" Margin="0,6,0,0" />
            <TextBox Text="Test 3" Grid.Row="2" Margin="0,6,0,0" />
            <TextBox Text="Test 4" Grid.Row="3" Margin="0,6,0,0" />
        </Grid>
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Button Content="Edit" IsEnabled="True" Grid.Column="0"
                HorizontalAlignment="Left" Width="75" />
            <Button Content="Save" IsEnabled="False" Grid.Column="1"
                Width="75" />
            <Button Content="Cancel" IsEnabled="False" Grid.Column="2"
                Width="75" Margin="6,0,0,0" />
        </Grid>
    </Grid>
</Window>

查看/EntryView.xaml.cs:

using System.Windows;

namespace MyCompany.MyProject.View

    public partial class EntryView : Window
    
        public EntryView()
        
            InitializeComponent();
        
    

我没有在这些控件上创建任何Name 属性。那是故意的。我将使用 MVVM,并且不会使用任何后面的代码。我会让设计师做它想做的事,但我不会碰任何代码。

创建视图模型

接下来我将制作我的视图模型。这应该以它为视图服务的方式设计,但理想情况下可以独立于视图。我不会担心太多,但重点是您不必必须拥有一对一的视图控件和视图模型对象。

我试图让我的视图/视图模型在更大的应用程序上下文中有意义,所以我将在这里开始使用视图模型。我们将把这个“可编辑的表单”变成一个 rolodex 条目。

我们将创建一个我们首先需要的辅助类...

ViewModel/DelegateCommand.cs:

using System;
using System.Windows.Input;

namespace MyCompany.MyProject.ViewModel

    public class DelegateCommand : ICommand
    
        private readonly Action<object> _execute;
        private readonly Func<object, bool> _canExecute;

        public DelegateCommand(Action execute)
            : this(execute, CanAlwaysExecute)
        
        

        public DelegateCommand(Action execute, Func<bool> canExecute)
        
            if (execute == null)
                throw new ArgumentNullException("execute");

            if (canExecute == null)
                throw new ArgumentNullException("canExecute");

            _execute = o => execute();
            _canExecute = o => canExecute();
        

        public bool CanExecute(object parameter)
        
            return _canExecute(parameter);
        

        public void Execute(object parameter)
        
            _execute(parameter);
        

        public event EventHandler CanExecuteChanged;

        public void RaiseCanExecuteChanged()
        
            if (CanExecuteChanged != null)
                CanExecuteChanged(this, new EventArgs());
        

        private static bool CanAlwaysExecute()
        
            return true;
        
    

ViewModel/EntryViewModel.cs:

using System;
using System.ComponentModel;
using System.Windows.Input;

namespace MyCompany.MyProject.ViewModel

    public class EntryViewModel : INotifyPropertyChanged
    
        private readonly string _initialName;
        private readonly string _initialEmail;
        private readonly string _initialPhoneNumber;
        private readonly string _initialRelationship;

        private string _name;
        private string _email;
        private string _phoneNumber;
        private string _relationship;

        private bool _isInEditMode;

        private readonly DelegateCommand _makeEditableOrRevertCommand;
        private readonly DelegateCommand _saveCommand;
        private readonly DelegateCommand _cancelCommand;

        public EntryViewModel(string initialNamename, string email,
            string phoneNumber, string relationship)
        
            _isInEditMode = false;

            _name = _initialName = initialNamename;
            _email = _initialEmail = email;
            _phoneNumber = _initialPhoneNumber = phoneNumber;
            _relationship = _initialRelationship = relationship;

            MakeEditableOrRevertCommand = _makeEditableOrRevertCommand =
                new DelegateCommand(MakeEditableOrRevert, CanEditOrRevert);

            SaveCommand = _saveCommand =
                new DelegateCommand(Save, CanSave);

            CancelCommand = _cancelCommand =
                new DelegateCommand(Cancel, CanCancel);
        

        public string Name
        
            get  return _name; 
            set
            
                _name = value;
                RaisePropertyChanged("Name");
            
        

        public string Email
        
            get  return _email; 
            set
            
                _email = value;
                RaisePropertyChanged("Email");
            
        

        public string PhoneNumber
        
            get  return _phoneNumber; 
            set
            
                _phoneNumber = value;
                RaisePropertyChanged("PhoneNumber");
            
        

        public string Relationship
        
            get  return _relationship; 
            set
            
                _relationship = value;
                RaisePropertyChanged("Relationship");
            
        

        public bool IsInEditMode
        
            get  return _isInEditMode; 
            private set
            
                _isInEditMode = value;
                RaisePropertyChanged("IsInEditMode");
                RaisePropertyChanged("CurrentEditModeName");

                _makeEditableOrRevertCommand.RaiseCanExecuteChanged();
                _saveCommand.RaiseCanExecuteChanged();
                _cancelCommand.RaiseCanExecuteChanged();
            
        

        public string CurrentEditModeName
        
            get  return IsInEditMode ? "Revert" : "Edit"; 
        

        public ICommand MakeEditableOrRevertCommand  get; private set; 
        public ICommand SaveCommand  get; private set; 
        public ICommand CancelCommand  get; private set; 

        private void MakeEditableOrRevert()
        
            if (IsInEditMode)
            
                // Revert
                Name = _initialName;
                Email = _initialEmail;
                PhoneNumber = _initialPhoneNumber;
                Relationship = _initialRelationship;
            

            IsInEditMode = !IsInEditMode; // Toggle the setting
        

        private bool CanEditOrRevert()
        
            return true;
        

        private void Save()
        
            AssertEditMode(isInEditMode: true);
            IsInEditMode = false;
            // Todo: Save to file here, and trigger close...
        

        private bool CanSave()
        
            return IsInEditMode;
        

        private void Cancel()
        
            AssertEditMode(isInEditMode: true);
            IsInEditMode = false;
            // Todo: Trigger close form...
        

        private bool CanCancel()
        
            return IsInEditMode;
        

        private void AssertEditMode(bool isInEditMode)
        
            if (isInEditMode != IsInEditMode)
                throw new InvalidOperationException();
        

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        private void RaisePropertyChanged(string propertyName)
        
            if (PropertyChanged != null)
                PropertyChanged(this,
                    new PropertyChangedEventArgs(propertyName));
        

        #endregion INotifyPropertyChanged Members
    

与这种类型的工作流程一样,我在最初创建视图时遗漏了一些要求。例如,我发现有一个“还原”功能可以撤消更改,但保持对话框打开是有意义的。我还发现我可以为此目的重复使用编辑按钮。所以我创建了一个属性,我将读取它来获取编辑按钮的名称。

视图模型包含很多代码来做一些简单的事情,但大部分都是用于连接属性的样板。不过,这个样板文件给了你一些力量。它有助于将您与您的视图隔离开来,因此您的视图可以在视图模型没有更改或仅进行微小更改的情况下发生巨大变化。

如果视图模型变得太大,您可以开始将其推入额外的子视图模型。在最有意义的地方创建它们,并将它们作为此视图模型上的属性返回。 WPF 数据绑定机制支持链接数据上下文。稍后我们联系起来时,您会发现有关此数据上下文的信息。

将视图连接到我们的视图模型

要将视图连接到视图模型,您必须在视图上设置DataContext 属性以指向您的视图模型。

有些人喜欢在 XAML 代码中实例化和指定视图模型。虽然这可以工作,但我喜欢保持视图和视图模型彼此独立,所以我确保使用一些第三类来连接两者。

通常我会使用依赖注入容器来连接我的所有代码,这需要大量工作,但要让所有部分保持独立。但是对于这么简单的应用程序,我喜欢使用 App 类将我的东西绑定在一起。让我们去编辑它:

App.xaml:

<Application x:Class="MyCompany.MyProject.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Startup="ApplicationStartup">
    <Application.Resources>

    </Application.Resources>
</Application>

App.xaml.cs:

using System.Windows;

namespace MyCompany.MyProject

    public partial class App : Application
    
        private void ApplicationStartup(object sender, StartupEventArgs e)
        
            // Todo: Somehow load initial data...
            var viewModel = new ViewModel.EntryViewModel(
                "some name", "some email", "some phone number",
                "some relationship"
                );

            var view = new View.EntryView()
            
                DataContext = viewModel
            ;

            view.Show();
        
    

您现在可以运行您的项目,尽管我们构建的逻辑不会做任何事情。这是因为我们创建了初始视图,但它实际上并没有进行任何数据绑定。

设置数据绑定

让我们返回并编辑视图以完成所有连接。

编辑视图/EntryView.xaml:

<Window x:Class="MyCompany.MyProject.View.EntryView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Rolodex Entry"
        Height="350" Width="525"
        MinWidth="300" MinHeight="200">
    <Grid Margin="12">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid Grid.Row="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <TextBlock Text="Name:" Grid.Column="0" Grid.Row="0" />
            <TextBox Text="Binding Name, UpdateSourceTrigger=PropertyChanged"
                     IsEnabled="Binding IsInEditMode" Grid.Column="1"
                     Grid.Row="0" Margin="6,0,0,0" />
            <TextBlock Text="E-mail:" Grid.Column="0" Grid.Row="1"
                       Margin="0,6,0,0" />
            <TextBox Text="Binding Email, UpdateSourceTrigger=PropertyChanged"
                     IsEnabled="Binding IsInEditMode" Grid.Column="1"
                     Grid.Row="1" Margin="6,6,0,0" />
            <TextBlock Text="Phone Number:" Grid.Column="0" Grid.Row="2"
                       Margin="0,6,0,0" />
            <TextBox Text="Binding PhoneNumber, UpdateSourceTrigger=PropertyChanged"
                     IsEnabled="Binding IsInEditMode" Grid.Column="1" Grid.Row="2"
                     Margin="6,6,0,0" />
            <TextBlock Text="Relationship:" Grid.Column="0" Grid.Row="3"
                       Margin="0,6,0,0" />
            <TextBox Text="Binding Relationship, UpdateSourceTrigger=PropertyChanged"
                     IsEnabled="Binding IsInEditMode" Grid.Column="1" Grid.Row="3"
                     Margin="6,6,0,0" />
        </Grid>
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Button Content="Binding CurrentEditModeName"
                    Command="Binding MakeEditableOrRevertCommand"
                    Grid.Column="0" HorizontalAlignment="Left"
                    Width="75" />
            <Button Content="Save" Command="Binding SaveCommand"
                    Grid.Column="1" Width="75" />
            <Button Content="Cancel" Command="Binding CancelCommand"
                    Grid.Column="2" Width="75" Margin="6,0,0,0" />
        </Grid>
    </Grid>
</Window>

我在这里做了很多工作。首先,静态的东西:

我更改了表单的标题以匹配 Rolodex 的想法 我为字段添加了标签,因为我现在知道它们适用于什么 我更改了最小宽度/高度,因为我注意到控件被切断了

接下来是数据绑定:

我将所有文本字段绑定到视图模型上的适当属性 我创建了文本字段update the view model on every keypress (UpdateSourceTrigger=PropertyChanged)。这对于此应用程序不是必需的,但将来可能会有所帮助。我添加它是为了避免您在需要时查找它:) 我将每个文本框的IsEnabled 字段绑定到IsInEditMode 属性 我将按钮绑定到它们各自的命令 我将编辑按钮的名称(Content 属性)绑定到视图模型上的相应属性

这是结果

现在所有 UI 逻辑都可以正常工作了,除了我们留下了 Todo 评论的那些。我没有实现这些,因为它们与特定的应用程序架构有关,我不想在这个演示中涉及到这些。

此外,香草 WPF 没有一种非常干净的 MVVM 方式来关闭我所知道的表单。您可以使用代码隐藏来执行此操作,也可以使用数十个 WPF 附加库之一,这些库提供了自己的更简洁的执行方式。

依赖属性

您可能已经注意到我没有在我的代码中创建单个自定义依赖属性。我使用的依赖属性都在现有控件上(例如TextContentCommand)。这就是它通常在 WPF 中的工作方式,因为数据绑定和样式(我没有涉及)为您提供了很多选择。它让您可以完全自定义内置控件的外观、感觉和操作。

在以前的 Windows GUI 框架中,您通常必须继承现有控件或创建自定义控件以获得自定义外观。在 WPF 中制作自定义控件的唯一原因是以可重用的方式组合多个控件的模式,或者从头开始创建一个全新的控件。

例如如果您正在制作一个与弹出控件配对的自动完成文本框以显示它自动完成的值。在这种情况下,您可能希望使用自定义依赖属性(例如自动完成源)制作自定义控件。这样您就可以在整个应用程序和其他应用程序中重复使用该控件。

如果您不制作自定义控件,或制作可以直接实例化并在 XAML 和数据绑定中使用的特殊非 UI 类,您可能不需要创建依赖属性。

【讨论】:

很好的分步解决方案,衷心感谢您所付出的时间/努力。在这种发展心态中,我有很多东西要学习/理解。 @DRapp:如果你愿意,我也可以写一个依赖属性的例子 :) 可能需要更长的时间,因为我不经常使用它们。 谢谢,但没必要。我需要消化一些基础知识,用我的环境(心态)对其进行采样,然后尝试爬行、行走,然后跑步。 @DRapp:我还试图传达创建依赖属性(以及为什么要这样做)和使用现有属性之间的区别.你会一直使用它们。我已经编辑了答案,试图让这一点更清楚。 哇... +1 表示详尽和详细【参考方案2】:

发帖人要求我重新发表我的评论作为答案。乐意效劳:-)

我参考的视频演示:http://blog.lab49.com/archives/2650 奖励链接:MSDN 中很棒的 WPF 文章:http://msdn.microsoft.com/en-us/magazine/dd419663.aspx 如果您不知道,在线文档中有一个章节:http://msdn.microsoft.com/en-us/library/ms752914.aspx

我还发现这本书很有帮助:http://www.amazon.com/WPF-4-Unleashed-Adam-Nathan/dp/0672331195

我自己的 WPF 经验涉及在我尝试让我的程序运行时在一堆不同的资源之间返回。 WPF 中有很多东西,在你学习它的时候很难把它们都记在脑子里。

【讨论】:

【参考方案3】:

查看它们的一种简单方法是,它们是指向另一个属性的属性。

它们实际上是一个属性的定义,定义了属性名称、类型、默认值等,但属性的实际值并没有和属性定义一起存储。

所以你可以说 Button 的 Enabled 属性将指向特定类的属性,或者它会指向 CheckBoxA.IsChecked 属性,或者你甚至可以说它只是指向一个布尔值 False。

// Value points to the current DataContext object's CanSaveObject property
<Button IsEnabled="Binding CanSaveObject" />

// Value points to the IsChecked property of CheckBoxA
<Button IsEnabled="Binding ElementName=CheckBoxA, Path=IsChecked" />

// Value points to the value False
<Button IsEnabled="False" />

【讨论】:

以上是关于试图理解 DependencyProperty的主要内容,如果未能解决你的问题,请参考以下文章

试图理解C中的递归

递归 - 试图理解

试图理解 TabBarDelegate

试图理解 TranslationInView

试图理解 ML 上的示例脚本

试图理解删除实体框架中的子实体