Monday, January 27, 2014

Использование динамических возможностей языка C# в WPF

Если Вы разработчик WPF/MVVM, то, надеюсь, найдете нечто новое в этой статье. Если же Вы не знакомы с WPF, то можете ознакомиться с данным материалом, чтобы посмотреть, как  можно подружить WPF с динамическими типами языка C#. Поговорим о том, как создавать нужные модели для байндинга на лету. Ранее в своем блоге я часто использовал пример с электронной библиотекой. Одним из сложных моментов создания такого приложения является необходимость создавать модель представления и модель для каждого представления. Поэтому зачастую архитектура такого приложения напоминает вид, показанный на рисунке ниже.
Для проекта, в котором много маленьких моделей, нужно придумать более сложное решение, а не реализовывать на каждый "чих" новую модель представления и модель. Для простеньких проектов на помощь приходит ExpandoObject, который позволяет избежать рутинного создания модели. Это класс, позволяющий создавать наши объекты в рантайме. Кроме этого, данный класс наследуется от интерфейса INotifyPropertyChanged, поэтому данные будут отображены на экране в нормальном виде и есть возможность получать необходимые уведомления об изменении тех или иных свойств. Чтобы указать на изменение свойств, нам нужно было явно наследоваться от интерфейса INotifyPropertyChanged и для каждого свойства вызывать PropertyChanged.
public class LibraryBook : INotifyPropertyChanged
    {
        private string _author;
        public string Author
        {
            get { return _author; }
            set
            {
                _author = value;
                OnPropertyChanged("Author");
            }
        }

        private string _title;
        public string Title
        {
            get { return _title; }
            set
            {
                _title = value;
                OnPropertyChanged("Title");
            }
        }

        private DateTime _year;
        public DateTime Year
        {
            get { return _year; }
            set
            {
                _year = value;
                OnPropertyChanged("Year");
            }
        }

        private string _sn;
        public string SN
        {
            get { return _sn; }
            set
            {
                _sn = value;
                OnPropertyChanged("SN");
            }
        }

        private int _count;
        public int Count
        {
            get { return _count; }
            set
            {
                _count = value;
                OnPropertyChanged("Count");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
Многовато кода для такой простенькой модели. Если использовать для этих целей PostSharp, то код упростится. Необходимые нам атрибуты пометим атрибутом [NotifyPropertyChanged]. По сути, мы сделаем то же самое, что и в описанном выше коде, просто вместо нас это сделает PostSharp. PostSharp позволяет вставлять функционал в существующий код путем переписывания IL-а.
От использования данного класса мы не избавляемся. Зная, что у нас такой простенький класс и нужно только вывести его для отображения, то что нам мешает максимально упростить процесс. Приступим к реализации данного функционала. Для начала необходимо реализовать внешний вид того, как мы хотим отобразить данную модель через XAML-код. Ниже приведена простая реализация данного функционала.

  <Window x:Class="WpfApplicationExpandoObject.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <ListView x:Name ="library" Grid.Column ="0" ItemsSource="{Binding Books}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <WrapPanel>
                        <TextBlock Text="Author: " />
                        <TextBlock Text="{Binding Author}" FontWeight="Bold" />
                        <TextBlock Text=", " />
                        <TextBlock Text="Caption: " />
                        <TextBlock Text="{Binding Title}" FontWeight="Bold" />
                        <TextBlock Text="Count: " />
                        <TextBlock Text="{Binding Count}" FontWeight="Bold" />
                    </WrapPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Grid Grid.Column="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid Grid.Row="0" DataContext="{Binding ElementName=library, Path=SelectedItem}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Label Grid.Row="0" Grid.Column="0" Content="Author" />
                <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Author}" />
                <Label Grid.Row="1" Grid.Column="0" Content="Title" />
                <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Title}" />
                <Label Grid.Row="2" Grid.Column="0" Content="Year" />
                <DatePicker Grid.Row="2" Grid.Column="1" SelectedDate="{Binding Year}" />
                <Label Grid.Row="3" Grid.Column="0" Content="Serial Number" />
                <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding SN}" />
                <Label Grid.Row="4" Grid.Column="0" Content="Count" />
                <TextBox Grid.Row="4" Grid.Column="1" Text="{Binding Count}" />
            </Grid>
        </Grid>
    </Grid>
</Window>
Наше представление (view) будет иметь следующий вид :
Остался последний штрих: создание необходимой модели на лету. Для этого в App.xaml удалим StartupUri, затем перейдем в App.xaml.cs и переопределим метод OnStartup. А далее смотрим на код и наслаждаемся его простотой.
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        dynamic viewModel = new ExpandoObject();
        viewModel.Books = GetBooks();
        var view = new MainWindow();
        view.DataContext = viewModel;
        view.Show();
    }

    private dynamic GetBooks()
    {
        dynamic books = new ObservableCollection<dynamic>();
        dynamic book = new ExpandoObject();
        book.Author = "Jon Skeet";
        book.Title = "C# in Depth";
        book.Count = 3;
        book.SN = "ISBN: 9781617291340";
        book.Year = new DateTime(2013, 9, 10);
        books.Add(book);

        book = new ExpandoObject();
        book.Author = "Martin Fowler";
        book.Title = "Refactoring: Improving the Design of Existing Code";
        book.Count = 2;
        book.SN = "ISBN-10: 0201485672";
        book.Year = new DateTime(1999, 7, 8);
        books.Add(book);

        book = new ExpandoObject();
        book.Author = "Jeffrey Richter";
        book.Title = "CLR via C# (Developer Reference)";
        book.Count = 5;
        book.SN = "ISBN-10: 0735667454";
        book.Year = new DateTime(2012, 12, 4);
        books.Add(book);
        return books;
    }
}

Сказать, что это просто, – ничего не сказать. При изменении нужного свойства после запуска приложения оно сразу отображает измененные данные. Это говорит о том, что наша модель отлично работает с интерфейсом INotifyPropertyChanged. Преимущество данного подхода проявляется в гибкости данного подхода. Данные для отображения можно достать с XML, JSON, SOAP и т.д. Подход с созданием для некоторых целей модели представления в большинстве случаев будет оправданным хорошим решением, в отличие от подхода с использованием ExpandoObject. При подходе, описанном в статье, мы теряем возможности статической типизации, и это грозит снижением скорости выполнения данной операции, а также потерей поддержки intellisense. Мы теряем одни возможности, а вместо них получаем другие. В каждой задаче нужно искать компромиссное решение. Поэтому сначала думаем, затем делаем. И Ваши программные решения будут работать стабильно, независимо от подхода к их реализации.

No comments:

Post a Comment