尝试理解 DependencyProperty

10

作为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;
}
现在有依赖属性和单/双向绑定。我了解(我想)单向绑定可以模拟更多只读操作。
然而,在双向绑定中,依赖项会自动通知任何依赖于源或目标更改的人,而不需要明确检查是否有事件订阅者,框架会自动处理将更改通知到相应控件(目标或源)的过程。
那么,让我用一个旧的添加/编辑保存/取消维护表单的场景来说明这一点。在旧的框架中,如果有人点击添加或编辑按钮,则所有数据输入字段都将变为“已启用”,新记录将为空数据,或编辑现有数据。同时,添加/编辑按钮将被禁用,但保存/取消按钮现在将变为已启用状态。
同样,当通过保存/取消完成时,它将禁用所有输入字段、保存/取消,并重新启用添加/编辑按钮。
我还不太明白这种类型的场景如何在这种依赖属性方案下处理,但我接近了吗?我也了解到你可以绑定到几乎所有东西,包括颜色方案、显示/隐藏、字体等等...但我正在尝试逐步理解这些内容。
谢谢。

1
因为WPF是一个如此庞大的主题,而且你的问题比较广泛,所以很难回答。我很乐意给你提供我最喜欢的WPF资源链接。例如,你是否了解过Model-View-ViewModel模式?这里有一份优秀的演示文稿:http://blog.lab49.com/archives/2650 - Corey Kosak
你的问题似乎是关于依赖属性的文章。我甚至都没有读它。 - Muhammad Hasan Khan
@Corey Kosak,如果您将您的评论发布为答案,我会检查它以获取解决方案,因为它提供了最好的逐步理解,而无需购买书籍。 - DRapp
这个问题表明了你做了研究,所以它绝对值得被问出来。但是它太宽泛了。为了确保我理解正确——你的问题是否归结为:“什么是依赖属性?如何在这种情况下使用它们:一个开始只读并且可以变成可编辑的表单?”看起来你得到的答案(包括你接受的答案)只是进一步阅读材料。 - Merlyn Morgan-Graham
添加了一个答案,试图阐明数据绑定的使用方式,并解释为什么在您描述的情况下不需要依赖属性。 - Merlyn Morgan-Graham
3个回答

6

getter/setter是普通C#属性的一种特性,它并不是WPF独有的。

单向/双向绑定是指WPF数据绑定,它不需要你创建依赖属性 - 只需要使用它们即可。

依赖属性已经内置于控件本身中。当您向表单添加自定义控件实例时,它们允许您直接引用这些属性。它们使您的自定义控件感觉更加“本地化”。

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

…如果有人点击添加或编辑按钮,则所有数据输入字段都将变为“启用”状态,其中包括新记录的空白数据或编辑现有数据。同时,添加/编辑按钮将变为禁用状态,但保存/取消按钮现在将变为启用状态。

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

我会使用以下方式来实现您想要实现的功能:

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

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

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

设置项目

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

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

Project settings to set the correct namespaces

我也有一种自定义的文件夹方案,我喜欢在 WPF 项目中使用:

enter image description here

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

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

创建视图

首先进入设计器,让一切看起来很好。暂时不要添加任何代码,也不要做其他工作。只需在设计器中玩耍,直到一切看起来正确(尤其是当您调整大小时)。这是我最终得到的东西:

The view I ended up with

View/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>

View/EntryView.xaml.cs:

using System.Windows;

namespace MyCompany.MyProject.View
{
    public partial class EntryView : Window
    {
        public EntryView()
        {
            InitializeComponent();
        }
    }
}

我没有在这些控件上创建任何Name属性,这是有意的。我将使用MVVM,并且不会使用任何代码后台。我会让设计师做它想做的事情,但我不会触碰任何那些代码。
创建视图模型
接下来,我将制作我的视图模型。这应该以一种服务于视图的方式进行设计,但理想情况下可以是视图独立的。我不会太担心这个问题,但重点是你不需要在视图控件和视图模型对象之间有1对1的对应关系。
我试图使我的视图/视图模型在更大的应用程序上下文中有意义,所以我将从这里开始为视图模型定目标。我们将把这个"可编辑表单"作为一个转盘条目。
我们将首先创建一个需要的辅助类...

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();
        }
    }
}

你现在可以运行你的项目,但我们构建的逻辑不会做任何事情。这是因为我们创建了初始视图,但它实际上没有进行任何数据绑定。
设置数据绑定
让我们返回并编辑视图以完成所有连接。
编辑View/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 的概念
  • 我添加了字段的标签,因为我现在知道它们适用于什么
  • 我更改了最小宽度/高度,因为我注意到控件被裁剪了

接下来是数据绑定:

  • 我将所有文本字段绑定到视图模型上相应的属性
  • 我使文本字段 在每次按键时更新视图模型 (UpdateSourceTrigger=PropertyChanged)。虽然对于此应用程序来说这不是必需的,但在未来可能会有帮助。 我添加了它,以免您需要查找它 :)
  • 我将每个文本框的 IsEnabled 字段绑定到 IsInEditMode 属性
  • 我将按钮绑定到它们各自的命令
  • 我将编辑按钮的名称 (Content 属性) 绑定到视图模型上相应的属性

这是结果

Read-only mode Edit mode

现在所有的UI逻辑都可以工作,除了那些我们留下了“Todo”注释的部分。我没有实现它们,因为它们涉及特定的应用程序架构,而我不想在这个演示中深入探讨。
此外,普通的WPF没有一个非常干净的MVVM方式来关闭一个窗体。你可以使用代码后台来完成它,或者你可以使用数十个WPF附加库中的一个,提供自己更清晰的完成方式。
依赖属性
您可能已经注意到,在我的代码中,我没有创建一个自定义的依赖属性。我使用的依赖属性都是存在于现有控件上的(例如Text、Content和Command)。这通常是WPF中的工作方式,因为数据绑定和样式(我没有深入研究)给您提供了很多选项。它使您完全可以自定义内置控件的外观、感觉和操作。
在以前的Windows GUI框架中,您经常需要对现有控件进行子类化或创建自定义控件才能获得自定义的外观和感觉。在WPF中制作自定义控件的唯一原因是以可重复使用的方式结合多个控件的模式,或从头开始创建一个全新的控件。
例如,如果您正在制作一个自动完成文本框,并将其与弹出控件配对以显示它正在自动完成的值。在这种情况下,您可能需要创建一个自定义控件,具有自定义依赖属性(例如自动完成源)。这样,您可以在整个应用程序和其他应用程序中重复使用该控件。
如果您不是在制作自定义控件或制作特殊的非UI类,可以直接在XAML中实例化和使用数据绑定,则可能不需要创建依赖属性。

非常棒的逐步解决方案,真诚地感谢您花费的时间和精力。在这种开发思维方式下,我还有很多需要学习和理解的地方。 - DRapp
@DRapp:如果您想要,我可以写一个依赖属性的例子给您 :) 可能需要更长一点时间,因为我不经常使用它们。 - Merlyn Morgan-Graham
@DRapp:我也试图解释一下创建依赖属性的区别(以及为什么要这样做)和使用现有属性的区别。你将一直使用它们。我已经编辑了答案,以尝试使其更清晰。 - Merlyn Morgan-Graham
经过更多的阅读,我正在逐步尝试您的示例...我无法在任何地方找到“DelegateCommand”参考...检查了一堆参考资料,什么也没有...它在哪里(我甚至包括了您在这里的命名空间)。 - DRapp
1
哇...对于彻底和详细的工作,加1分。 - Corey Kosak
显示剩余2条评论

2
海报要求我将我的评论重新发布为答案。很乐意效劳 :-)

此外,我发现这本书非常有帮助:http://www.amazon.com/WPF-4-Unleashed-Adam-Nathan/dp/0672331195

我自己使用WPF的经验涉及在尝试使我的程序工作时在不同的资源之间来回切换。 WPF中有太多的东西,当你学习它时,真的很难将所有内容都记在脑海中。


1

简单来说,它们是指向另一个属性的属性。

实际上,它们是属性的定义,定义了属性名称、类型、默认值等,但属性的实际值并未存储在属性定义中。

因此,您可以说按钮的启用属性将指向特定类的属性,或者它将指向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" />

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接