Быстрый старт с WPF. Часть 1. Привязка, INotifyPropertyChanged и MVVM

  • Tutorial

Всем привет!


По разным причинам большинство из нас использует десктопные приложения, как минимум, браузер :) А у некоторых из нас возникает необходимость в написании своих. В этой статье я хочу пробежаться по процессу разработки несложного десктопного приложения с использованием технологии Windows Presentation Foundation (WPF) и применением паттерна MVVM. Желающих продолжить чтение прошу под кат.


Думаю, необязательно говорить, что WPF — это разработка Microsoft :) Технология эта предназначена для разработки десктопных приложений под Windows, начиная с Windows XP. Почему именно так? Это связано с тем, что WPF работает поверх платформы .NET, минимальные требования которой — Windows XP и новее. К сожалению, WPF не работает на других платформах, хотя есть шансы, что в ближайшее время это изменится: в стадии разработки находится WPF-based фреймворк Avalonia.


В чём особенность WPF?


Два основных отличия WPF от других средств построения десктопных приложений:


  • Язык разметки XAML, предназначенный для разметки самого интерфейса окна.
  • Рендеринг посредством DirectX, аппаратное ускорение графики.

Я не буду углубляться в подробности, т.к. это не совсем тема статьи. Если интересно, то гуглить XAML, WPF rendering, milcore.dll и DirectX :)


О чём эта статья?


Эта статья содержит пример приложения, построенного на технологии WPF:



Я постараюсь ориентировать материал статьи в практическую сторону в стиле "повторяй за мной" с пояснениями.


Что нам понадобится для повторения статьи?


Небольшой опыт разработки на C# :) Как минимум, нужно хорошо понимать синтаксис языка. Также понадобится Windows-машина (в примерах будет Win 10) с установленной на ней Visual Studio (в примерах будет 2017, есть бесплатная Community версия). При установке VS необходимо будет включить поддержку десктопной разработки под платформу .NET


image


Так же в этом разделе я опишу создание проекта.


Запускаем VS, создаём новый проект, тип приложения выбираем WPF App (.NET Framework) (можно ввести в строке поиска справа вверху), называем как угодно.


image


После создания нового проекта откроется окно редактора интерфейса, у меня оно выглядит так


image


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


image


Перед тем, как начать


Элементы окна (их ещё называют контрОлами от слова Control) должны размещаться внутри контейнера или внутри другого элемента типа ContentControl. Контейнер — это специальный контрол, позволяющий разместить внутри себя несколько дочерних контролов и организовать их взаимное расположение. Примеры контейнеров:


  • Grid — позволяет организовать элементы по столбцам и строкам, ширина каждого столбца или строки настраивается индивидуально.
  • StackPanel — позволяет расположить дочерние элементы в одну строку или столбец.

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


MVVM и интерфейс INotifyPropertyChanged. Копия текста.


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


Итак, у нас есть свежесозданный проект (я назвал его Ex1), перейдём в редактор разметки и первым делом заменим контейнер, указанный по умолчанию (<Grid></Grid>) на <StackPanel></StackPanel>. Этого контейнера будет достаточно, т.к. нам понадобится расположить всего лишь два контрола один над другим. Укажем явно, каким образом будут располагаться компоненты, добавив свойство Orientation="Vertical". Добавим внутрь стек панели парочку элементов: поле для ввода текста и поле для отображения текста. Поскольку эти контролы не будут содержать вложенного кода, можно описать их самозакрывающимся тегом (см. код ниже). После всех вышеописанных процедур код описания контейнера и вложенных контролов должен принять такой вид:


<StackPanel Orientation="Vertical">
    <TextBox />
    <TextBlock />
</StackPanel>

Теперь сосредоточимся на цели этого примера. Мы хотим, чтобы при наборе текста в текстбоксе этот же текст синхронно отображался в текстблоке, избежав при этом явной операции копирования текста. Нам понадобится некая связующая сущность, и вот тут-то мы и подошли к такой штуке, как привязка (binding), о которой было сказано выше. Привязка в терминологии WPF — это механизм, позволяющий связывать некоторые свойства контролов с некоторыми свойствами объекта C#-класса и выполнять взаимное обновление этих свойств при изменении одной из частей связки (это может работать в одну, в другую или в обе стороны сразу). Для тех, кто знаком с Qt, можно провести аналогию слотов и сигналов. Чтобы не растягивать время, перейдём к коду.


Итак, для организации привязки нужны свойства контролов и некое свойство некоего C#-класса. Для начала разберёмся с XAML-кодом. Текст обоих контролов хранится в свойстве Text, поэтому добавим привязку для этих свойств. Делается это так:


<TextBox Text="{Binding}"/>
<TextBlock Text="{Binding}"/>

Мы сделали привязку, но пока непонятно к чему :) Нам нужен объект какого-то класса и какое-то свойство в этом объекте, к которому будет выполнена привязка (как ещё говорят, на которое нужно забиндиться).


Так что это за класс? Этот класс называется вьюмоделью (view model) и служит как раз связующим звеном между view (интерфейсом или его частями) и model (моделью, т.е. теми частями кода, которые отвечают за логику приложения. Это позволяет отделить (в какой-то степени) логику приложения от интерфейса (представления, view) и называется паттерном Model-View-ViewModel (MVVM). В рамках WPF этот класс также называется DataContext.


Однако, просто написать класс вьюмодели недостаточно. Нужно ещё как-то оповещать механизм привязки о том, что свойство вьюмодели или свойство вью изменилось. Для этого существует специальный интерфейс INotifyPropertyChanged, который содержит событие PropertyChanged. Реализуем этот интерфейс в рамках базового класса BaseViewModel. В дальнейшем все наши вьюмодели мы будем наследовать от этого базового класса, чтобы не дублировать реализацию интерфейса. Итак, добавим в проект каталог ViewModels, а в этот каталог добавим файл BaseViewModel.cs. Получим такую структуру проекта:


image


Код реализации базовой вьюмодели:


using System.ComponentModel;

namespace Ex1.ViewModels
{
    public class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Создадим для нашего класса MainWindow свою вьюмодель, унаследовавшись от базовой. Для этого в том же каталоге ViewModels создадим файл MainWindowViewModel.cs, внутри которого будет такой код:


namespace Ex1.ViewModels
{
    public class MainWindowViewModel : BaseViewModel
    {

    }
}

Шикарно! Теперь нужно добавить в эту вьюмодель свойство, на которое будем биндить текст наших контролов. Поскольку это текст, тип этого свойства должен быть string:


public string SynchronizedText { get; set; }

В итоге получим такой код


namespace Ex1.ViewModels
{
    public class MainWindowViewModel : BaseViewModel
    {
        public string SynchronizedText { get; set; }
    }
}

Так, кажется, справились. Осталось забиндиться на это свойство из вьюхи и готово. Давайте сделаем это прямо сейчас:


<TextBox Text="{Binding Path=SynchronizedText}"/>
<TextBlock Text="{Binding Path=SynchronizedText}"/>

Ништяк, запускаем проект, набираем текст в текстбокс иииии… ничего не происходит))) Ну, ничего страшного, на самом деле мы идём правильной дорогой, просто пока ещё не дошли до нужной точки.


Предлагаю на минутку остановиться и подумать, чего же нам не хватает. Вьюха у нас есть. Вьюмодель тоже. Свойства вроде забиндили. Нужный интерфейс реализовали. Проделали кучу работы ради копирования жалкой строчки текста, за что нам это???!?!111


Ладно, шутки в сторону. Мы забыли создать объект вьюмодели и кое-что ещё (об этом позже). Сам класс мы описали, но это ничего не значит, ведь у нас нет объектов этого класса. Ок, где нужно хранить ссылку на этот объект? Ближе к началу примера я упомянул некий DataContext, используемый в WPF. Так вот, у любой вью есть свойство DataContext, которому мы можем присвоить ссылку на нашу вьюмодель. Сделаем это. Для этого откроем файл MainWindow.xaml и нажмём F7, чтобы открыть код этой вьюхи. Он практически пустой, в нём есть только конструктор класса окна. Добавим в него создание нашей вьюмодели и поместим её в DataContext окна (не забываем добавить using с нужным неймспейсом):


public MainWindow()
{
    InitializeComponent();
    this.DataContext = new MainWindowViewModel();
}

Это было просто, но этого всё равно не хватает. По-прежнему при запуске приложения никакой синхронизации текста не происходит. Что ещё нужно сделать?


Нужно вызвать событие PropertyChanged при изменении свойства SynchronizedText и сообщить вьюхе о том, что она должна следить за этим событием. Итак, чтобы вызвать событие, модифицируем код вьюмодели:


public class MainWindowViewModel : BaseViewModel
{
    private string _synchronizedText;
    public string SynchronizedText
    {
        get => _synchronizedText;
        set
        {
            _synchronizedText = value;
            OnPropertyChanged(nameof(SynchronizedText));
        }
    }
}

Что мы тут сделали? Добавили скрытое поле для хранения текста, обернули его в уже существующее свойство, а при изменении этого свойства не только меняем скрытое поле, но и вызываем метод OnPropertyChanged, определённый в базовой вьюмодели и вызывающий событие PropertyChanged, объявленное в интерфейсе INotifyPropertyChanged, так же реализованное в базовой вьюмодели. Получается, что при каждом изменении текста возникает событие PropertyChanged, которому передаётся имя свойства вьюмодели, которое было изменено.


Ну, почти всё, финишная прямая! Осталось указать вьюхе, что оно должно слушать событие PropertyChanged:


<TextBox Text="{Binding Path=SynchronizedText, UpdateSourceTrigger=PropertyChanged, Mode=OneWayToSource}"/>
<TextBlock Text="{Binding Path=SynchronizedText, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"/>

Помимо того, что мы указали, по какому триггеру должно происходить обновление, мы так же указали, в какую сторону это обновление отслеживается: от вью к вьюмодели или наоборот. Поскольку в текстбоксе мы вводим текст, то нам интересны изменения только во вью, поэтому выбираем режим OneWayToSource. В случае с текстблоком всё ровно наоборот: нам интересны изменения во вьюмодели, чтобы отобразить их во вью, поэтому выбираем режим OneWay. Если бы нам нужно было, чтобы изменения отслеживались в обе стороны, можно было не указывать Mode вообще, либо указать TwoWay явно.


Итак, запускаем программу, набираем текст и voi-la! Текст синхронно меняется, и мы нигде ничего не копировали!


image


Спасибо за внимание, продолжение следует. Будем разбираться с DataTemplate и паттерном Command.

Поделиться публикацией

Комментарии 45

    +1
    Посмотрите в сторону ReactiveUI фреймворка. Намного интереснее будет повторять.
      +4
      Спасибо за комментарий)
      Я пишу про базовые вещи, имхо, на фреймворки нужно переходить уже после того, как хотя бы немного потыкал голую платформу
        +1

        Использовать со старта вспомогательные библиотеки и/или фреймворки или нет сложный вопрос.


        Лично я начинал с библиотеки FSharp.ViewModule и успешно ее использовал в своих небольших проектах, даже не подозревая о внутреннем устройстве команд и объектов уведомляющих о своем изменении. И еще долгое время подобные тонкости меня не беспокоили. Я считаю, что главное понимать что механизм делает, а осознание как именно придет с опытом, при условии что вообще возникнет необходимость разбираться во внутренней реализации.


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


        К тому-же, на мой взгляд, интереснее разбираться в том как приложение работает, а не в том как заставить его работать. И, конечно, самостоятельно написанная работающая программа, в отличие от взятой из папки "samples", пусть даже и построенная из готовых блоков (вызовов готовых методов), дает положительный эффект — "я могу, я умею" [хотя-бы что-то!], добавляя уверенности, а не отрицательный — "ничего не работает", "это не для меня".

          0

          Я как раз и писал эту статью в расчёте на то, что она поможет справиться с тем, как заставить приложение работать.
          Мнение интересное, спасибо)

        0
        Совершенно другая идиология и если предварительно не переломить мозг с ReactiveX, то лучше даже и не пытаться.
          0

          Реактивные расширения и в частности ReactiveUI позволяют решать задачи существенно быстрее. "Переломить мозг" не так уж и сложно, тем более, что потом это быстро окупится. Туда же PropertyChanged.Fody. После того, как попробуешь эти две библиотеки, пути назад уже не будет. Начинаешь понимать, что вот эти ручные реализации INotifyPropertyChanged — нечто из далёкого прошлого. См. https://m.habr.com/post/418007/

        +3
        Спасибо за статью. Очень вовремя подошла. Буду ждать следующие.
          0

          Пожалуйста)

          +3
          Модель сейчас можно уже расписывать немного по другому, для экономия места

          private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
                  {
                      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
                  }
          
                  protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
                  {
                      if (EqualityComparer<T>.Default.Equals(field, value)) return false;
                      field = value;
                      NotifyPropertyChanged(propertyName);
                      return true;
                  }
          
                  public string Id
                  {
                      get => _id;
                      set => SetField(ref _id, value);
                  }
            0

            Ага, так и будем понемногу модифицировать код, чтобы понятно было, зачем это и откуда

              0
              Жду еще статей, самому так и не получилось в MVVM толком разобраться)
                0

                Думаю, что раз Вы понимаете суть привязки и PropertyChanged, то вполне себе уже понимаете и MVVM)

              +3
              Тоже хотел сделать замечание по поводу CallerMemberName в OnPropertyChanged.

              Вы забыли создать поле. Да и как показала практика SetField бесполезная штука от слова полностью, слишком часто используются свойства, которые не хранят значения в самой VM, а просто передают их из моделей (т.к. основная задача VM это обеспечить взаимодействие View <--> Model).

              public bool IsSomething => something.SomeMethod();
              // где-то в конструкторе
              something.SomeEvent += (s, e) => OnPropertyChanged(nameof(IsSomething));
              

              Cлишком часто же, в сеттере делается работа, в основном для View, иногда требуется даже контролировать когда именно вызывать PropertyChanged, когда именно менять поле и т.д. — совершенно бесполезно автоматизировать это все.
              +1
              Хорошая статья. Но только… где Model от «первой буковки» паттерна? )))
                0
                Спасибо) в этом примере обошлось без модели, т.к. нет никакой логики и сущностей, которые нужно было бы хранить. Дальше всё будет)
                  +1
                  А зря. Простейший класс со стринговым автосвойством подошёл бы отлично. А заодно проброс уведомлений был бы нагляден.
                  — По поводу фреймворков. На заре изучения WPF кинулся я по шаблону Catel клепать десктопные «калькуляторы», да так увяз во всём, что не замечал очевидного: нахрен не сдались мне эти фреймворки, ибо всё «из коробки» работало более чем отлично. Такие статьи тому пример — что не надо навешивать кучу библиотек, аспектов ))), nuGet пакетов, достаточно лишь основ для понимания всей логики паттерна и WPF десктопа в частности.
                    0
                    Я решил понемногу наращивать сложность, поэтому в этот раз без модели) Потому что инфы немало и без этого получилось
                      0
                      По поводу фреймворков и паттерна полностью согласен. Хотя часть рутинной работы, пожалуй, можно и на фреймворки сбросить)
                        +2
                        Конечно можно, и даже нужно! Чего один Fody будет стоить. Но когда у свежеиспечённого прогера WPF вложенный класс в модели не будет нотифить VM а та V, тогда уже свежеиспечённый прогер полезет на StackOverFlow за очередной порцией быдлокода фреймворка, вместо того, чтобы явно указать штатную подписку на событие изменения.
                      +1
                      Связь между V и VM проста и банальна, на ней даже особо заострять внимание не стоит, просто как данность (С тем же caliburn привязка еще удобнее делается). Организация работы с моделью, вот где самое интересное и камень преткновения).
                    +1
                    Чтоб уж полностью соответстовать паттерну, то View и задание DataContext лучше производить в App.xaml.cs, а то в данном случае получается, что View создает VM. Ну или использовать какой нибудь фреймворк (caliburn.micro/MVVMLight)
                      0
                      Ага, думал сделать так, но потом решил убрать в конструктор окна. Далее вообще перенесём присваивание датаконтекста в обработчик события ContentRendered, т.к. датаконтекст может быть громоздким и может затормозить отрисовку.
                        0
                        Хорошо. Интересно почитать будет. Не затягивайте с новыми статьями :) А то помню как несколько лет назад знакомился с MVVM, везде HelloWorld'ы, а чтоб что нибудь сложнее — нету.
                        .
                        За всё время только один попался проект с применением MVVM и без использования паттернов, на который приятно посмотреть.
                          0
                          Насчёт времени обещать конкретного плана, к сожалению, нет, но постараюсь не затягивать)
                            0
                            За ссылку спасибо, очень круто сделано) Прям на самом деле приятно посмотреть
                            +1
                            Так это тоже затормозит отрисовку))
                            Представьте, что вы отрисовали ваш наисложнейший интерфейс, а потом подтягиваете базу данных на 100 000 записей. Не заморачивайтесь с громоздскостью.
                            roadmap то довольно простой: структура проекта с папками и правильной обызвалкой классов, Model, notification, ViewModel, View, binding, commands, collections.
                            И главное: как можно меньше сторонних либ. Это крайне, Крайне упростит понимание что там происходит внутрях.
                              0
                              Ну основной интерфейс будет отрисован, а на время подтягивания базы я могу вывести крутилку или какие-то заглушки) Но в целом да, статья не об этом, поэтому лучше делать максимально наглядно и канонiчно
                          0
                          Если не считать нарушения паттерна MVVM, статья неплоха. Сам смысл MVVM заключается в отделении данных от логики и представления, а у вас получается Model скрещена с ViewModel и уведомляет об изменениях сама себя.

                          Странно говорить о Model-View-ViewModel, не используя Model.
                            0
                            Спасибо) Уже несколько раз отвечал на это, но повторюсь: сейчас пока всё «на пальцах», да и в текущем примере не нужна модель как таковая, потому что нечего хранить.
                              +2
                              Ну это достаточно распространенная практика, когда VM и M скрещивают, если все очень тревиально.
                                +1
                                Согласен, распространённая. Но почему то считают, что это надо прям навязывать при объяснении паттерна «для новичков» где прям в названии слово Model стоит. Речь обо всех возможных статьях в интернетах.
                                +1
                                Вы не поверите, в книге от MS по WPF тоже как-то забито на MVVM по полной.
                                  0
                                  О какой книге идёт речь?
                                    +1
                                    Сори, перепутал. Имел ввиду издательства Apress. WPF: Windows Presentation Foundation в .NET 4.5. Уж как-то про многое рассказано, а про MVVM забыто.
                                +2
                                Хотел тут написать про MVVMLight и забивании на велосипеды. Но авторы вооще ребята, к успеху идут, выпилили документацию и впаривают курсы.
                                  0

                                  Поэтому ReactiveUI и PropertyChanged.Fody — наши друзья и товарищи! Удивительная эффективность, опенсорс, никакой коммерции, кроссплатформенность и хорошая документация.

                                  +2
                                  Grid — позволяет организовать элементы по столбцам и строкам, ширина каждого столбца или строки настраивается индивидуально.

                                  Кажется, вы пропустили "в виде сетки или таблицы" и не только ширина, но и высота тоже ;)


                                  MVVM и интерфейс INotifyPropertyChanged. Копия текста.

                                  Копия текста?


                                  Привязка в терминологии WPF — это механизм, позволяющий связывать некоторые свойства контролов с некоторыми свойствами объекта C#-класса и выполнять взаимное обновление этих свойств при изменении одной из частей связки (это может работать в одну, в другую или в обе стороны сразу).

                                  Не обязательно C# класса. WPF может прекрасно подружиться с F#, не говоря уже о VB.Net.


                                  Ну, почти всё, финишная прямая! Осталось указать вьюхе, что оно должно слушать событие PropertyChanged:
                                  TextBox Text="{Binding Path=SynchronizedText, UpdateSourceTrigger=PropertyChanged, Mode=OneWayToSource}"
                                  TextBlock Text="{Binding Path=SynchronizedText, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"

                                  Здесь, мне кажется, вы не совсем однозначно выражаетесь.


                                  UpdateSourceTrigger лишь указывает на то, когда будет обновляться привязываемое свойство. PropertyChanged устанавливает режим обновления сразу после изменения свойства в "приемнике" (не самое удачное слово, но ничего лучше на ум так и не пришло). По умолчанию TextBox обновляет привязку после потери своего фокуса. Что касается TextBlock, то там установка UpdateSourceTrigger не требуется, как и явный Mode = OneWay.


                                  В целом, несмотря на то, что хороших материалов по WPF хватает, еще один туториал точно не повредит.


                                  Но в будущем мне бы хотелось, чтобы примеры были не настолько искусственные. В частности: здесь совсем не требуется реализация INotifyPropertyChanged. Если TextBlock должен дублировать текст из TextBox, то можно сразу там и привязываться к свойству Text, а дополнительная прослойка в виде свойства, оповещающего о своих изменениях, не нужна, обычного авто-свойства в данном конкретном случае будет достаточно.

                                    +1
                                    Спасибо за комментарий, отвечу по порядку)
                                    Кажется, вы пропустили «в виде сетки или таблицы» и не только ширина, но и высота тоже ;)

                                    Да, про высоту забыл, действительно
                                    Копия текста?

                                    В примере происходит копирование текста из одного контрола в другой
                                    Не обязательно C# класса. WPF может прекрасно подружиться с F#, не говоря уже о VB.Net.

                                    Согласен, но я нигде не говорил, что всё ограничивается C#. Просто я его использую, поэтому про него и пишу)

                                    По поводу замечания об UpdateSourceTrigger — я просто не вдаюсь в подробности, чтобы не останавливаться на деталях на таком этапе, когда человеку требуется туториал. Впрочем, комментарии прекрасно дополняют чтение) А так же умение искать материал в интернете.

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

                                      Ах, вот оказывается в чем дело, как я сразу не догадался =)

                                        +1
                                        Тогда напишите не «Копия текста», а «Пример: Копирование текста».
                                        И вообще, текст от ссылки до места якоря можно поместить в спойлер, раз вы считаете, что эта ссылка вообще нужна. А так, выглядит, что это ссылка на другую статью, или на репозиторий с текстом примера (который был бы полезен многим). Мне пришлось приглядываться к ссылке, чтобы понять, что вы не отправляете меня куда-то.
                                      +1
                                      Хорошая статья, будет интересна как для новичков так и для тех кто уже знаком с WPF, и хотели бы узнать о патерне MVVM.
                                      Также мне кажется стоило бы указать что привязать DataContext можно сразу в XAML.
                                        0
                                        Если вы про XAML самой View говорите, то это так же не верный подход как и тут.
                                        0
                                        Статья хорошая, хорошо рассказана основа биндинга.

                                        Есть несколько замечаний/предложений:
                                        1) лучше не загромождать xaml необязательными атрибутами. Например, StackPanel легко обойдётся без Orientation=«Vertical», а оба биндинга справятся без указания Mode. К тому же, режим биндингов и так будет разный — у TextBox режим по умолчанию TwoWay, а у TextBlock — OneWay (как указано в настройках свойств зависимости Text этих классов).

                                        2) Всё-таки лучше указывать датаконтекст в xaml-е, а не в конструкторе:
                                        <Window ...
                                                xmlns:vm="clr-namespace:Ex1.ViewModels">
                                            <Window.DataContext>
                                                <vm:MainWindowViewModel/>
                                            </Window.DataContext>
                                            <StackPanel>
                                                ...
                                            </StackPanel>
                                        </Window>
                                        

                                        Как указали выше, суть не изменится, зато будет два плюса — показать, что в замле можно указывать всё что угодно, а также в редакторе заработает подсказка свойств в биндинге (лучше отметить, что при изменениях во вьюмодели нужно сбилдить проект). А во втором уроке можно будет рассказать про d:DataContext, опять же — для подсказок.

                                        3) Базовый класс BaseViewModel лучше делать не сразу, а потом. В начале показать, что во вьюмодели должен быть INotifyPropertyChanged, а потом (во второй части) сделать рефакторинг и вынести в отдельный класс. Предполагаю, что эту статью будут читать новички, им будет полезно показывать по шагам, объясняя по ходу изменения в коде и показывая необходимость проводить рефакторинг.
                                          0
                                          Всё-таки лучше указывать датаконтекст в xaml-е, а не в конструкторе

                                          На самом деле, лучше всего использовать шаблон NavigationService или ViewModelLocator, чтобы можно было легко и непринуждённо использовать IoC-контейнер!

                                            0
                                            Ну, это pro-уровень :) в первом уроке использовать это не нужно (хотя, упомянуть в тексте можно). Автору пришлось указывать на новую сущность в обзорном уроке — объяснять «код позади» xaml. По опыту обучения новичков, чем меньше различных сущностей в одном уроке — тем лучше.

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

                                        Самое читаемое