Пытаясь понять DependencyProperty - PullRequest
       13

Пытаясь понять DependencyProperty

9 голосов
/ 29 сентября 2011

Будучи новичком в WPF и его поразительной способностью изменять, связывать, включать и иным образом манипулировать.Я пытаюсь получить мысленный обзор происходящего и надеюсь, что кто-то сможет подтвердить или исправить мои показания.

До WPF у вас есть делегаты и события.Вы можете иметь дюжину элементов управления для всех прослушиваний (путем регистрации на событие), поэтому при возникновении события все другие элементы управления будут уведомлены автоматически и могут действовать, как бы они ни были закодированы.Например ...

Из Code Behind вы можете сделать что-то вроде

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

Теперь есть свойства зависимости и односторонняя / двусторонняя привязка.Я понимаю (я думаю) об одностороннем моделировании большего количества операций только для чтения.

Во всяком случае, с двухсторонним связыванием зависимости автоматически уведомляют кого-либо, «зависящего» от изменения либо источника, либо целисоответственно, без явной проверки, если что-то подписалось на событие, платформа автоматически обрабатывает объявление об изменении соответствующих элементов управления (цель или источник).

Итак, давайте рассмотрим этот сценарий со старымДобавить / Изменить Сохранить / Отменить форму обслуживания.В более старой структуре, если кто-то нажимал на кнопку добавления или редактирования, все поля ввода данных становились бы «включенными» либо с пустыми данными для новой записи, либо с редактированием существующих данных.В то же время кнопки добавления / редактирования станут недоступными, но теперь кнопки «Сохранить / Отменить» станут активными.

Аналогичным образом, после завершения с помощью кнопки «Сохранить / Отменить» будут отключены все поля ввода, сохранение / отмена.и снова включите кнопки Добавить / Изменить.

Я не совсем понимаю, как такой сценарий такого типа будет обрабатываться в этом сценарии свойств зависимостей (пока), но я близок?Я также понимаю, что вы можете связать практически все, в том числе цветовые схемы, показать / скрыть, шрифты и т. Д. Но я предпринимаю небольшие шаги, чтобы действительно понять этот материал.

Спасибо.

Ответы [ 3 ]

6 голосов
/ 29 сентября 2011

Метод получения / установки является функцией обычных свойств C #.Он не уникален для WPF.

Этот односторонний / двусторонний материал говорит о привязке данных WPF, которая не требует создания свойств зависимости - просто для их использования.

Свойства зависимостей встроены в сами элементы управления.Они позволяют напрямую ссылаться на эти свойства при добавлении экземпляров вашего элемента управления в форму.Они позволяют вашему пользовательскому элементу управления чувствовать себя немного более «родным».

Обычно они используются для реализации свойства, которое может использовать привязку данных.В ваших приложениях вы будете просто использовать привязку данных, а не внедрять новые хуки для нее.

... если кто-то нажмет на кнопку добавления или редактирования, всеполя ввода данных станут «включенными» либо с пустыми данными для новой записи, либо с редактированием существующих данных.В то же время кнопки добавления / редактирования станут недоступными, но теперь кнопки «Сохранить / Отменить» станут активными.

Аналогично, после завершения с помощью кнопки «Сохранить / Отменить» будут отключены все поля ввода, сохранение / отмена.и снова включите кнопки Добавить / Редактировать.

Я бы выполнил то, что вы хотите сделать с помощью:

  • Модель представления
  • Привязка данныхв представлении для этой модели представления
  • Предоставление ICommand для этой модели представления (для кнопок)
  • INotifyPropertyChanged для модели представления (для всех свойств)

Нет новогосвойства зависимости должны быть созданы для этого сценария.Вы просто будете использовать существующие для привязки данных.

Вот пример кода / учебное пособие по выполнению WPF со связыванием данных и стилю MVVM.

Настройка проекта

Я создал приложение WPF в мастере создания проекта и назвал его MyProject.

Я настроил имя проекта и пространства имен в соответствии с общепринятой схемой вещей.Вы должны установить эти свойства в обозревателе решений -> проект -> щелчок правой кнопкой мыши -> свойства.

Project settings to set the correct namespaces

У меня также есть настраиваемая схема папок, которую я люблю использовать для проектов WPF:

enter image description here

Я поместил представление в его собственную папку «Вид» для организационных целей.Это также отражается в пространстве имен, поскольку ваши пространства имен должны соответствовать вашим папкам (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 и просматривать объекты модели.

Я пытаюсьчтобы мои представления / модели представления имели смысл в более широком контексте приложения, поэтому я начну рассматривать модель представления здесь.Мы сделаем эту «редактируемую форму» записью 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();
        }
    }
}

Теперь вы можете запустить свой проект, хотя построенная нами логика ничего не даст. Это потому, что наше начальное представление создано, но на самом деле оно не выполняет привязку данных.

Настройка привязки данных

Давайте вернемся и отредактируем представление, чтобы закончить все это.

Редактирование 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

Теперь работает вся логика пользовательского интерфейса, кроме тех, которые мы оставили в комментарии Todo. Я оставил их невыполненными, потому что они имеют отношение к конкретной архитектуре приложения, и я не хотел вдаваться в подробности для этой демонстрации.

Кроме того, в vanilla WPF нет очень чистого MVVM-способа закрыть форму, о которой я знаю. Вы можете использовать выделенный код, чтобы сделать это, или вы можете использовать одну из десятков библиотек надстроек WPF, которые обеспечивают их собственный более чистый способ сделать это.

Свойства зависимостей

Возможно, вы заметили, что я не создал ни одного пользовательского свойства зависимости в своем коде. Все свойства зависимостей, которые я использовал, относились к существующим элементам управления (например, Text, Content и Command). Вот как это обычно работает в WPF, потому что привязка данных и стиль (в который я не попал) дает вам много вариантов. Он позволяет полностью настраивать внешний вид, поведение и действия встроенных элементов управления.

В предыдущих платформах Windows GUI вам часто приходилось создавать подклассы существующих элементов управления или создавать собственные элементы управления, чтобы получить индивидуальный внешний вид. Единственными причинами создания пользовательских элементов управления в WPF является комбинирование шаблонов нескольких элементов управления с возможностью повторного использования или создание совершенно нового элемента управления с нуля.

например. если вы делали текстовое поле для автозаполнения, которое связано с всплывающим элементом управления, чтобы отобразить значения, из которых оно автоматически заполняется. В таком случае вы можете захотеть создать пользовательский элемент управления с пользовательскими свойствами зависимостей (например, источником автозаполнения). Таким образом, вы можете повторно использовать элемент управления в приложении и других приложениях.

Если вы не создаете пользовательские элементы управления или не создаете специальные классы, не связанные с пользовательским интерфейсом, которые могут непосредственно создавать экземпляры и использовать их в XAML и привязке данных, вам, вероятно, не потребуется создавать свойства зависимостей.

2 голосов
/ 29 сентября 2011

Автор попросил, чтобы я опубликовал свой комментарий в качестве ответа.С удовольствием обещаю: -)

Также я нашел эту книгу очень полезной: http://www.amazon.com/WPF-4-Unleashed-Adam-Nathan/dp/0672331195

Моймой собственный опыт работы с WPF включает в себя переход между кучей разных ресурсов, когда я пытаюсь заставить свою программу работать.В WPF столько всего, что действительно сложно держать это в голове, пока вы изучаете его.

1 голос
/ 29 сентября 2011

Простой способ взглянуть на них состоит в том, что они являются свойствами, указывающими на другое свойство.

На самом деле это определение свойства, которое определяет имя свойства, тип, значение по умолчанию и т. Д., Но фактическое значение свойства не сохраняется вместе с определением свойства.

Таким образом, вы можете сказать, что свойство Enabled Button будет указывать на свойство определенного класса, или оно будет указывать на свойство 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" />
...