пятница, 9 апреля 2010 г.

Шпаргалка по MVVM в WPF

Тема, конечно, неоднократно поднятая, но я все равно напишу шпаргалку на тот случай, если вдруг когда-нибудь забуду. А то с WCSF тоже вроде разобралась, а прошло два месяца со сдачи проекта, и уже ничего и не помню...

Сразу предупрежу, что статья предназначена для того, чтобы быстро въехать в MVVM или быстро его вспомнить. Так что описание будет предельно краткое и схематичное.

Уважаемые коллеги! Эта статья написана в 2010 году. Я примерно с 2011 пишу только на Java под андроид и не помню про WPF ничего. Что-либо у меня спрашивать — бесполезно. Извините

Итак, MVVM. Расшифровывается как Model-View-ViewModel. Рассмотрим на примере.

Пример

Для начала отмечу, что для упрощения нашей жизни с данным паттерном добрые люди разрабатывают MVVM Toolkit, включающий шаблон для Visual Studio:

WPF Model-View Application

Для данного проекта сразу создается следующая структура файлов:

Файлы WPF Model-View Application

Задача

Возьмем какую-нибудь каноничную задачу. Например, отображение списка книг читального зала. У книги есть:

  • Название
  • Автор
  • Доступное количество

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

На примере такой незатейливой задачи мы и рассмотрим MVVM

Model

Как несложно догадаться, Model — это сущности системы. У нас модель будет состоять из одного простого класса:

Book.cs

class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
    public int Count { get; set; }

    public Book(string title, string author, int count)
    {
        this.Title = title;
        this.Author = author;
        this.Count = count;
    }
}

ViewModel

ViewModel — это, пожалуй, ключевой момент всей этой истории. Это такие специальные классы, которые:

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

В учетом датабиндинга в WPF всё это дает замечательный результат: в C#-коде формы становится совсем не надо ничего писать. Впрочем, обо все по порядку. Напишем ViewModel для нашей модели:

Book.cs

class BookViewModel : ViewModelBase
{
    public Book Book;

    public BookViewModel(Book book)
    {
        this.Book = book;
    }

    public string Title
    {
        get { return Book.Title; }
        set
        {
            Book.Title = value;
            OnPropertyChanged("Title");
        }
    }

    public string Author
    {
        get { return Book.Author; }
        set
        {
            Book.Author = value;
            OnPropertyChanged("Author");
        }
    }

    public int Count
    {
        get { return Book.Count; }
        set
        {
            Book.Count = value;
            OnPropertyChanged("Count");
        }
    }
}

BookViewModel унаследован от класса ViewModelBase, который заботливо сгенерил нам MVVM Toolkit. ViewModelBase же, в свою очередь, реализует интерфейс INotifyPropertyChanged и содержит функцию OnPropertyChanged. Все это нужно для того, чтобы всегда можно было вызвать событие "изменилось такое-то поле". Как видно в коде, при любом изменении поля мы такое событие вызываем и передаем в качестве параметра его название. Потом на форме биндинг может это событие обработать и, как следствие, интерфейс и ViewModel всегда будут друг с другом синхронизированы. Впрочем, это я опять забегаю вперед.

Помимо BookViewModel у нас есть еще класс MainViewModel, уже сгенерированный и даже связанный с формой. Добавим в него поле:

ObservableCollection<BookViewModel> BooksList { get; set; }

ObservableCollection — это такая специальная коллекция, которая умеет отслеживать изменения в себе. Также слегка изменим конструктор:

public MainViewModel(List<Book> books)
{
    BooksList = new ObservableCollection<BookViewModel>(books.Select(b => new BookViewModel(b)));
}

View

Это и есть наше окно, либо User Control. У любого FrameworkElement-а WPF есть такое поле DataContext. DataContext может быть любым object-ом, иметь какие угодно поля, а его главная задача — являться источником данных для Databinding-а. Форма у нас всего одна, DataContext для нее заполняется в методе OnStartup, что в App.xaml.cs. Немного модифицируем то, что сделал нам MVVM Toolkit, получится следующее:

App.xaml.cs

private void OnStartup(object sender, StartupEventArgs e)
{
    List<Book> books = new List<Book>()
    {
        new Book("Колобок", null, 3),
        new Book("CLR via C#", "Джеффри Рихтер", 1),
        new Book("Война и мир", "Л.Н. Толстой", 2)
    };
       
    MainView view = new MainView(); // создали View
    MainViewModel viewModel = new ViewModels.MainViewModel(books); // Создали ViewModel
    view.DataContext = viewModel; // положили ViewModel во View в качестве DataContext
    view.Show();
}

Осталось написать XAML-код формы. Он прост и незатейлив:

MainView.xaml

<Window x:Class="SampleMVVM.Views.MainView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:c="clr-namespace:SampleMVVM.Commands"
    Title="Main Window" Height="400" Width="800">
   
    <ListView ItemsSource="{Binding BooksList}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <Border BorderBrush="Bisque" BorderThickness="1" Margin="10">
                    <StackPanel Margin="10">
                        <TextBlock Text="{Binding Title}" FontWeight="Bold"/>
                        <TextBlock Text="{Binding Author}" />
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="Осталось:" />
                            <TextBlock Text="{Binding Count}" FontWeight="Bold" Margin="10,0"/>
                            <TextBlock Text="шт" />
                        </StackPanel>
                    </StackPanel>
                </Border>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</Window>

Обратите внимание на конструкцию Binding в разметке формы. Именно таким образом можно привязывать поля объекта, находящегося в DataContext-е, к атрибутам контролов. Мы не написали ни строчки кода, но тем не менее при запуске получим следующее:

Отображение данных на форме

Редактирование

Сделаем так, что для выделенной в списке книги будет открываться редактор. Изменим XAML-разметку формы:

MainView.xaml

<Window x:Class="SampleMVVM.Views.MainView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:c="clr-namespace:SampleMVVM.Commands"
    Title="Main Window" Height="400" Width="350">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
       
        <ListView ItemsSource="{Binding BooksList}" IsSynchronizedWithCurrentItem="True">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="Bisque" BorderThickness="1" Margin="10">
                        <StackPanel Margin="10">
                            <TextBlock Text="{Binding Title}" FontWeight="Bold"/>
                            <TextBlock Text="{Binding Author}" />
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Text="Осталось:" />
                                <TextBlock Text="{Binding Count}" FontWeight="Bold" Margin="10,0"/>
                                <TextBlock Text="шт" />
                            </StackPanel>
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
       
<ContentControl Grid.Column="1" Content="{Binding BooksList}"> <ContentControl.ContentTemplate> <DataTemplate> <Border BorderBrush="Bisque" BorderThickness="1" Margin="10"> <StackPanel Margin="10"> <TextBlock Text="Название:"/> <TextBox Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10"/> <TextBlock Text="Автор:"/> <TextBox Text="{Binding Author, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10"/> </StackPanel> </Border> </DataTemplate> </ContentControl.ContentTemplate> </ContentControl>
</Grid> </Window>

Стоит обратить внимание на конструкцию UpdateSourceTrigger=PropertyChanged в строке биндинга. Это значит, что любое изменение, производимое в данном поле, будет немедленно отражаться на источнике. Это легко увидеть:

Редактирование данных

Если этого не написать, источник будет обновляться только по окончании редактирования (т.е. когда контрол будет терять фокус). Это может привести к следующей ошибке интерфейса: когда жмешь "Сохранить", сохраняется все, кроме только что измененного поля.

Команды

Добавим в приложение функциональности. Пусть некие читатели берут книги и возвращают. Соответственно, сделаем две кнопки — «Выдать» и «Забрать—, меняющие количество имеющихся в наличии книг. Если книг не осталось (Count = 0), кнопка «Выдать» должна дизаблиться.

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

Команда должна представлять из себя экземпляр класса, реализующего интерфейс ICommand. К счастью, MVVM Toolkit снова нам помог и сгенерил целых два таких класса — DelegateCommand для реализации команды без параметров и DelegateCommand<T> — для реализации команды с параметром типа T.

Мы параметры передавать не будем. Код во ViewModel будет таков:

BookViewModel.cs

#region Забрать

private DelegateCommand getItemCommand;

public ICommand GetItemCommand
{
    get
    {
        if (getItemCommand == null)
        {
            getItemCommand = new DelegateCommand(GetItem);
        }
        return getItemCommand;
    }
}

private void GetItem()
{
    Count++;
}

#endregion

#region Выдать

private DelegateCommand giveItemCommand;

public ICommand GiveItemCommand
{
    get
    {
        if (giveItemCommand == null)
        {
            giveItemCommand = new DelegateCommand(GiveItem, CanGiveItem);
        }
        return giveItemCommand;
    }
}

private void GiveItem()
{
    Count--;
}

private bool CanGiveItem()
{
    return Count > 0;
}

#endregion

Обратите внимание, что этот код добавляется в BookViewModel, а не в MainViewMode. Дело в том, что мы будем добавлять кнопки в ContentControl, DataContext-ом которого является именно BookViewModel.

С первой командой все ясно. Создали команду, и в назначили ей в качестве действия метод GetItem, который и будет вызываться при ее активации. Со второй немного интереснее, но тоже просто. Помимо того, что она выполняет некоторое действие, она еще и может проверять с помощью метода CanGiveItem(), может она выполняться или нет.

В XAML-разметку нашей формы добавим следующее

MainView.xaml

<ContentControl Grid.Column="1" Content="{Binding BooksList}">
    <ContentControl.ContentTemplate>
        <DataTemplate>
            <Border BorderBrush="Bisque" BorderThickness="1" Margin="10">
                <StackPanel Margin="10">
                    <TextBlock Text="Название:"/>
                    <TextBox Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10"/>

                    <TextBlock Text="Автор:"/>
                    <TextBox Text="{Binding Author, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10"/>

<StackPanel Orientation="Horizontal"> <Button Content="Выдать" Command="{Binding GiveItemCommand}" Margin="10,0" /> <Button Content="Забрать" Command="{Binding GetItemCommand}" Margin="10,0" /> </StackPanel>
</StackPanel> </Border> </DataTemplate> </ContentControl.ContentTemplate> </ContentControl>

Вот и все. Мы получили требуемую функциональность. Количество экземпляров книги увеличивается и уменьшается, а когда их становится 0, кнопка «Выдать» дизаблится (благодаря упомянутому CanGiveItem).

Команды

Итого

В приведенном приложении все данные и реализация логики вынесены в отдельное место. В C#-коде формы мы не добавили ни строчки. XAML понятен и прекрасен. Благодаря паттерну MVVM в коде легко разобраться и легко сопровождать.

Исходники примера

Ссылки

33 комментария:

Анонимный комментирует...

Вы -- молодец!

Анонимный комментирует...

Обрабатывать SelectionChanged не совсем правильно. На помощь приходит CollectionViewSorce с методом GetDefaultView. Он возвращает представление для коллекции, котоое реализует ICollectionView. А уж в ICollectionView есть все необходимое для работы (нотификация изменения текущей позиции, фильтры и прочие полезности).
Cлой ViewModel позволяет полностью описать бизнес-логику приложения (Мастер-детайлы, общие справочники и т.п.). Во View пишется только то управление для GUI, которое невозможно реализовать из ViewModel(т.к. VIewModel не должна ничего знать о своем представлении): управление фокусировками, хоткеи и т.п.

darja комментирует...

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

Анонимный комментирует...

Один из вариантов - пишется общая обертка для показа View в модальном режиме. Обертка представляет собой команду, которая показывает модальную форму (которая может содержать кнопки Ок, Применить, Отмена в различных комбинациях), передавая для нее датаконтекст и шаблон представления. Также указывается команда, которая будет выполнятся в случае "положительного" результата обработки формы.

Команда биндится в XAMLе, в cs-файле ни единой строчки по прежнему нет.

Это в общих чертах, там много тонкостей :)

darja комментирует...

Я в общих чертах так и делаю.
Проблема в том, что, чтобы модальное окно закрылось, надо задать ему DialogResult. И для этого приходится обрабатывать OnClick кнопки и в обработчике писать DialogResult = true.

darja комментирует...

Хотя DialogResult тоже забиндить можно...

Анонимный комментирует...

Некоторые вещи разумнее писать в коде. Стремление абсолютно все вынести в разметку может принести вред (наступал на эти грабли).

Кстати, OnClick кнопки - "невьюмодельно", нужно завести команду и прибиндить ее к кнопке.

Форму ведь необязательно по клику кнопки будут закрывать.

darja комментирует...

Вот это меня, собственно, и смущало. OnClick некрасиво и не вписывается в MVVM; биндить WindowResult можно, но он во ViewModel тоже не в тему совершенно, а окно по Ok закрыть хочется. Что же делать?

Анонимный комментирует...

"Что же делать?"

Cinch

darja комментирует...

Если Вы имели в виду вот этот Cinch, то меня несколько смущает отсутствие релизов. Не хочу завязываться в рабочем приложении на нестабильный фреймворк.

Андрей комментирует...

Здравствуйте Дарья! Спасибо Вам за Вашу статью! У меня в процессе её чтения возник ряд вопросов, без которых не получается понять весь объём представленной Вами информации до конца. Был бы весьма признателен Вам за ответы на них:

В XAML-разметке вашего класса MainView, присутствуют элементы ListView и ContentControl.

Для ListView назначен источник данных: ItemsSource="{Binding BooksList}".
Для ContentControl этот же объект назначен в качестве содержимого: Content="{Binding BooksList}".

В подобных ситуациях для ListView я пишу такую разметку:

А для ContentControl пишу так:

Приведённая мною выше разметка мне понятна: в одном элементе (ListView) я указываю источником данных тип ObservableCollection, а во втором (ContentControl) - отслеживаю значение текущего выбора, произведённого в первом элементе.

Ваша разметка более лаконична и так же работает. Однако я никак не могу понять почему... В элементе ContentControl в качестве источника данных Вы указали Content="{Binding BooksList}". Судя по поведению ContentControl, он автоматически привязался к Items.CurrentItem? Искал в MSDN информацию, в которой бы упоминалось такое поведение - не нашел (к сожалению).

Правильно ли я понимаю - если свойству Content объекта ContentControl назначать класс, реализующий интерфейс ItemsControl (как это сделано в Вашем примере), то на самом деле объект ContentControl автоматически будет искать данные в ItemsControl.Items.CurrentItem? Если "да", то касается ли это только ItemsControl?

Андрей комментирует...

п.с. в предыдущем сообщении текстовый редактор сайта затёр мой код:

В подобных ситуациях для ListView я пишу такую разметку:
ListView Name="myList" ItemsSource="{Binding BooksList}"

А для ContentControl пишу так:
ContentControl Grid.Column="1" Content="{Binding ElementName=myList, Path=SelectedItem}"

darja комментирует...

2 Андрей:
Все дело в атрибуте IsSynchronizedWithCurrentItem. Почитайте про него.

Андрей комментирует...

я читал и помню, что он синхронизирует выбранный Item с источником - ItemsControl.Items.CurrentItem в случае Вашего примера. Но мой вопрос не в этом, а в том, что объект ContentControl, получив в качестве источника набор данных, определяет, что следует искать не абы какое свойство (для представления данных), а именно ItemsControl.Items.CurrentItem...

Ведь если в качестве источника в примере рассматривался бы string[], то в этом случае ContentControl не смог бы пройтись по иерархии свойств и обратиться к CurrentItem, поскольку в string[] этого свойства нет.

Alexander Alexeychuk комментирует...

Дарья, этот Ваш текст с кусками кода можно отнести к категории интересного и полезного.

Спасибо Вам!

Анонимный комментирует...

Спасибо, Дарья, за полезную шпаргалку!
Подскажите плиз, если кому не сложно, при такой модели как написать чтобы из MainViewModel закрывать приложение и вызывать стандартные команды окна (Close, Minimize, Move) ?

Dron247 комментирует...

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

Tekuto Koori комментирует...

Дарья, огромное Вам спасибо за UpdateSourceTrigger=PropertyChanged. Я долго искал, как добиться такого поведения TextBox'а

Анонимный комментирует...

Большое спасибо за шпаргалку!
Вопрос, как в рамках mvvm обработать следующий случай: в момент работы с вьюшкой произошли изменения с источником модели(допустим, у экземпляра Book по каким-то независимым причинам "изнутри" изменилось свойство Title). Как BookViewModel узнает об этом изменении?

Анонимный комментирует...

Андрей,
>>Но мой вопрос не в этом, а в том, что объект ContentControl, получив в качестве источника набор данных, определяет, что следует искать не абы какое свойство (для представления данных), а именно ItemsControl.Items.CurrentItem

если объект одноэлементного множества (в данном случае ContentControl) связан с представлением коллекции, он автоматически привязывается к CurrentItem представления

источник: http://msdn.microsoft.com/ru-ru/library/ms752347.aspx#data_conversion

Анонимный комментирует...

"Еще бы знать, как на MVVM модальные окна правильно делать." - посмотрите, может это как раз то что Вам нужно - http://www.silverlightshow.net/items/ModalDialogs-IEditableObject-and-MVVM-in-Silverlight-4.aspx

(правда, это SL, но нестрашно)

PS. Спасибо Вам за блог, очень приятно и полезно ))

netfantom комментирует...

Признаюсь, в голове некоторая каша.
Я вот пытаюсь понять как это применяется например во взаимодействии с EntityFramework. По идее сгенерированные объекты EntityFramework как-раз должны представлять прослойку Model, но как именно это реализуется пока не понимаю. Может есть какие-то примеры?

Роман комментирует...

Добрый день Дарья!
На моем блоге есть похожая тема(реализация Mvvm, только в silverlight), предлагаю нам обменяться ссылками. Я разместил вашу ссылку у себя на странице статьи: http://rio900.com/archives/156/mvvm-silverlight-%D0%BF%D1%80%D0%B0%D0%BA%D1%82%D0%B8%D0%BA%D0%B5/

ViDom комментирует...

А у меня этот пример не работает ((
Сделал пример до редактирования данных (только просмотр)..

Объект viewModel имеет данные, а ListView.Items.Count равняется нулю.. и в окне не показывает список книг..

Alex комментирует...

Отличная статья, но в корне не согласен с Вашим подходом к Model и ViewModel.

Model - действительно бизнес сущность системы и ее логика не зависит от ViewModel.
Т.е. если у книги, например, есть сводное служебное поле из фамилии автора и названия, то в Вашей версии в каждой из ViewModel (редакторов может быть несколько) Вам придется реализовать что-то вроде:
public string Author
{
set
{
Book.Author = value;
OnPropertyChanged("Author");
OnPropertyChanged("Summary"); // не забыть добавить во все
}

+ кроме того продублировать все эти свойства в каждой из ViewModel.
А это уже дублирование и явный повод для рефакторинга )
Итого, решение:
1. добавляем базовый класс для моделей с реализацией INotifyPropertyChanged.
2. Наследуем модель от п.1.
3. из ViewModel убираем все врапперы свойств модели с нотификациями.
4. из View биндимся Binding Book.Title. тем более что свойство Book у вас уже есть.

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

в подкрепление моей версии ссылки на почитать и посмотреть:

http://archive.msdn.microsoft.com/mag200902MVVM

http://www.codeproject.com/KB/WPF/MVVMForDummies.aspx

Nes комментирует...

Согласен с вами Alex, на себе уже проверено - так действительно лучше.

JOHN комментирует...

А как расширить проект добавлением формы создания новой книги?! Где будет лежать команда Add и как и в какую именно вью модель прокинуть данные с контролов главной формы?

R0mantik комментирует...

Спасибо Вам, наконец доходчивый пример MVVM. Не посоветуете ли где еще, можно почитать про MVVM?
(кроме MSDN конечно). Только, только начинаю постигать суть.

Devel комментирует...

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

Devel комментирует...

Дополнительную информацию можно прочитать в статье Take MVC to the Next Level in .NET, а так же на codeplex, где размещен Xomega Framework.

user13823 комментирует...

Alex написал ерунду.
“если у книги, например, есть сводное служебное поле из фамилии автора и названия” – это полная фигня. И ничему вообще не соответствует. И нарушает принципы ООП.
У вас класс, который содержит свойства Фамилия и Название. Всё. А дальше в совcем клиентском коде можете делать с ними все, что угодно, составлять из них какие вам угодно гибридные поля и т.д.
Иначе, если принять ваше предложение, начнется содом и гоморра. А именно: вы считаете, что нужно только служебное поле Фамилия+Название? А на каком основании? А я считаю, что нужно еще одно служебное поле: Названи+Фамилия.
И это еще не все! Я таких служебных полей могу придумать миллион. Следуя вашей логике, мне надо будет создать класс с миллионом полей, из которых только 2 самостоятельные, а все остальные получаются из этих 2-х.
Бред.
Поэтому, правильно применяя принципы ООП, и не плодя на пустом месте бессмысленных составных полей (из уже имеющихся), вам и не надо будет ничего дописывать вида OnPropertyChanged("Summary") и т.д. Все будет работать автоматически, как это и предусмотрено паттерном MVVM.
Далее.
“если появится требование всегда оставлять в наличии книги Маркса в одном экземпляре” – заметьте, что это не элемент бизнес-логики класса «Книга»! Это элемент бизнес-логики класса «Библиотека» (потому что в одной библиотеке такое правило может быть, а в другой его может и не быть, так что собственно книга тут ни при чем).
Следовательно, в случае появления такого требования, оно должно быть реализовано, естественно, в модели, но модель эта будет классом «Библиотека». Это же очевидно. Т.е. приложение надо будет доработать именно в этом направлении, а не кодить ту фигню, которую вы предлагаете.
По приведенным вами ссылкам находятся сомнительные статьи с описанием паттерна недо-полу-какбы-MVVM. Ну посудите сами: то, что там описывается, приводит к тому, что View и Model знают друг о друге! Ну так ведь MVVM для того и был придуман, чтобы разъединить View и Model, чтобы они друг о друге вообще ничего не знали, а все взаимодействие осуществлялось бы через класс-посредник (ViewModel)!
Как же можно называть MVVM-паттерном образец кода, в котором View и Model знают друг о друге??
Я бы не стал на них ориентироваться.

Andrey Daedra комментирует...

user13823?,
если очень хочется сделать поле, которое характеризует объект, как, например, "сводное служебное поле из фамилии автора и названия", можно просто переопределить метод ToString(). Бонусом получим более простую привязку там, где этого будет достаточно. Принципам ООП это противоречить не будет.

Алексей Зеленов комментирует...

Спасибо. Почти не реально найти путный материал на русском языке!