Tuesday, December 15, 2015

Как я пытался использовать Perspex с MVVM

Здравствуйте, уважаемые читатели. Сегодня я немного расскажу о таком проекте, как Perspex. Мне об этом проекте поведали на фейсбуке, и он не давал мне спокойно спать несколько дней, так как очень хотелось попробовать, что он собой представляет. Далее я столкнулся с другой проблемой: простой тестовый проект не работал. Но давайте вернемся пока к самому проекту. Perspex − это мульти-платформенный проект, который позволяет, используя XAML, писать приложения для разных платформ. Этот проект подобен WPF, к которому мы привыкли. Он сейчас доступен как альфа-версия для скачивания. Я решил сразу установить себе extension для Visual Studio, который называется PerspexVS. После установки вы сразу можете начать пробовать его в действии.
Сразу после того, как создадите свое приложение, обновите сразу Perspex через NuGet Package Manger, потому что функционала для написания даже самого простого MVVM приложения вам не хватит.
После этого вам в проект будет добавлено много библиотек.
Внутри Perspex библиотек много логики написано с использованием Rx библиотек. Моей основной целью было взять какое-то свое старенькое решение допилить с использованием Perspex. Поэтому в структуру проекта я добавил следующую структуру папок:
Для тех, кто плотно работал с паттерном MVVM, эта структура не вызовет недопонимания. Эта структура в любом случае вам понадобится. Так как я пробую воссоздать свой старый проект, который будет показывать работу с электронной библиотекой, мы начнем с создания сущностей, которые нам понадобятся. Зайдем в папку Models и добавим интерфейс IBook.
public interface IBook
{
    string Author { get; set; }
    string Title { get; set; }
    DateTime Year { get; set; }
    string SN { get; set; }
    int Count { get; set; }
}
Затем в эту же папку добавим реализацию этого класса.
public class Book : ReactiveObject, IBook
{
    private string _author;
    public string Author
    {
        get { return _author; }
        set
        {
            this.RaiseAndSetIfChanged(ref _author, value);
        }
    }

    private string _title;
    public string Title
    {
        get { return _title; }
        set
        {
            this.RaiseAndSetIfChanged(ref _title, value);
        }
    }

    private DateTime _year;
    public DateTime Year
    {
        get { return _year; }
        set
        {
            this.RaiseAndSetIfChanged(ref _year, value);
        }
    }

    private string _sn;
    public string SN
    {
        get { return _sn; }
        set
        {
            this.RaiseAndSetIfChanged(ref _sn, value);
        }
    }

    private int _count;
    public int Count
    {
        get { return _count; }
        set
        {
            _count = value;
            this.RaiseAndSetIfChanged(ref _count, value);
        }
    }
}
Тут мы использовали объект ReactiveObject с библиотеки ReactiveUI (идет в поставке с Perspex). Теперь нам нужен сервис, который будет возвращать информацию по книгам. Назовем этот сервис IBookService и добавим его в папку Services.
public interface IBookService
{
    void GetData(Action<ObservableCollection<IBook>, Exception> callback);
    IBook FindBook(IBook findBook);
    void CreateNewBook();
    void RemoveBook(IBook book);
}
Затем в эту же папку добавим имплементацию этого интерфейса в классе BookService.
public class BookService : IBookService
{
    #region Variable
    private ObservableCollection<IBook> _books;
    #endregion

    #region Constructor
    public BookService()
    {
        _books = new ObservableCollection<IBook>();
        _books.Add(new Book { Author = "Jon Skeet", Title = "C# in Depth", Count = 3, SN = "ISBN: 9781617291340", Year = new DateTime(2013, 9, 10) });
        _books.Add(new Book { Author = "Martin Fowler", Title = "Refactoring: Improving the Design of Existing Code", Count = 2, SN = "ISBN-10: 0201485672", Year = new DateTime(1999, 7, 8) });
        _books.Add(new Book { Author = "Jeffrey Richter", Title = "CLR via C# (Developer Reference)", Count = 5, SN = "ISBN-10: 0735667454", Year = new DateTime(2012, 12, 4) });
    }
    #endregion

    #region Public Methods
    public void GetData(Action<ObservableCollection<IBook>, Exception> callback)
    {
        callback(_books, null);
    }

    public IBook FindBook(IBook findBook)
    {
        if (findBook == null)
            return null;

        return _books.FirstOrDefault(book => book.Author == findBook.Author
                                    && book.Title == findBook.Title
                                    && book.SN == findBook.SN
                                    && book.Year == findBook.Year);
    }

    public void CreateNewBook()
    {
        _books.Add(new Book { Author = "Test1", Title = "Test1", Count = 5, SN = "ISBN-10: 0735667454", Year = DateTime.Now });
    }

    public void RemoveBook(IBook book)
    {
        if (book == null)
            return;

        _books.Remove(book);
    }
    #endregion
}
Теперь перейдем в нашу папку ViewModels и добавим нашу модель представления, которую назовем MainViewModel. Ее реализация приведена ниже.
public class MainViewModel : ReactiveObject
{
    #region [ vars ]
    private readonly IBookService _bookService;
    #endregion

    #region [ .ctor ]
    /// <summary>
    /// Initializes a new instance of the MainViewModel class.
    /// </summary>
    public MainViewModel(IBookService dataService)
    {
        _bookService = dataService;
        _bookService.GetData(
            (items, error) =>
            {
                Books = items;
            });

        AddBookCommand = ReactiveCommand.Create();
        AddBookCommand.Subscribe(_ =>
        {
            _bookService.CreateNewBook();
        });
    }
    #endregion

    #region Public Properties
    public IBook FavoriteBook { get; set; }

    public ObservableCollection<IBook> Books { get; set; }

    private string Name { get; set; }

    private IBook _selectedBook;
    public IBook SelectedBook
    {
        get { return _selectedBook; }
        set
        {
            this.RaiseAndSetIfChanged(ref _selectedBook, value);
        }
    }
    #endregion

    #region Command
    public ReactiveCommand<object> AddBookCommand { get; }

    private ReactiveCommand<IBook> _removeBookCommand;
    public ReactiveCommand<IBook> RemoveBookCommand
    {
        get
        {
            return _removeBookCommand ?? (_removeBookCommand =
                ReactiveCommand.CreateAsyncObservable(this.WhenAnyValue(
                            x => x._selectedBook,
                            x => CanRemoveBook(x)),
                            RemoveBook));
        }
    }

    private IObservable<IBook> RemoveBook(object arg)
    {
        if (SelectedBook != null)
            Books.Remove(SelectedBook);
        return Observable.Return(SelectedBook);
    }

    public bool CanRemoveBook(IBook book)
    {
        return true;
    }
    #endregion
}
Здесь нет ничего сложного. Единственное, что может у вас вызвать проблемы,  это если вы не использовали никогда Rx; тогда читать этот код действительно трудно.
AddBookCommand = ReactiveCommand.Create();
AddBookCommand.Subscribe(_ =>
{
    _bookService.CreateNewBook();
});
И этот кусок кода:
public ReactiveCommand<IBook> RemoveBookCommand
{
    get
    {
        return _removeBookCommand ?? (_removeBookCommand =
            ReactiveCommand.CreateAsyncObservable(this.WhenAnyValue(
                        x => x._selectedBook,
                        x => CanRemoveBook(x)),
                        RemoveBook));
    }
}
Теперь давайте свяжем нашу модель представления, которую мы только создали с нашим представлением. Для этого откроем файл MainWindow.paml.cs и добавим следующий код:
public class MainWindow : Window
{
    public MainWindow()
    {
        this.InitializeComponent();
        this.DataContext = new MainViewModel(new BookService());
        App.AttachDevTools(this);
    }

    private void InitializeComponent()
    {
        PerspexXamlLoader.Load(this);
    }
}
Теперь самое время реализовать наше представление. 
Здесь появляется главная проблема: как адаптировать приведенный ниже код к тому виду, который поддерживает Perspex.
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <ListBox x:Name="library" Grid.Column ="0" ItemsSource="{Binding Books}">
        <ListBox.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>
        </ListBox.ItemTemplate>
    </ListBox>
    <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>

        <StackPanel Grid.Row="1" Orientation="Horizontal">
            <Button Content="Add new book" Margin="3"  Command="{Binding AddBookCommand}" />
            <Button Content="Remove book" Margin="3" Command="{Binding RemoveBookCommand}"/>
        </StackPanel>
   
    </Grid>
</Grid>
Здесь начались мои танцы с бубном, и я потратил два дня, чтобы это хоть как-то заработало. Все, что я смог выжать с их редактора, показано ниже.
<Window xmlns="https://github.com/perspex"
        xmlns:vm="clr-namespace:PerspexApplication.ViewModels;assembly=PerspexApplication">
  <Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <ListBox Grid.Column ="0" Items="{Binding Books}" SelectedItem="{Binding SelectedBook}"/>
    <Grid Grid.Column="1">
          <Grid.RowDefinitions>
              <RowDefinition Height="Auto" />
              <RowDefinition Height="Auto" />
          </Grid.RowDefinitions>
          <Grid Grid.Row="0" DataContext="{Binding library.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>
              <TextBlock Grid.Row="0" Grid.Column="0" Text="Author" />
              <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Author}" />
              <TextBlock Grid.Row="1" Grid.Column="0" Text="Title" />
              <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Title}" />
              <TextBlock Grid.Row="2" Grid.Column="0" Text="Serial Number" />
              <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding SN}" />
              <TextBlock Grid.Row="3" Grid.Column="0" Text="Count" />
              <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Count}" />
          </Grid>

          <StackPanel Grid.Row="1" Orientation="Horizontal">
              <Button Content="Add new book" Margin="3"  Command="{Binding AddBookCommand}" />
              <Button Content="Remove book" Margin="3" Command="{Binding RemoveBookCommand}"/>
          </StackPanel>
      </Grid>
  </Grid>
</Window>
В итоге у меня все свелось к такому корявому отображению:
Почему нормально не отображается название с левой стороны, думаю, понятно (DataTemplate не работает в Perspex). Я решил поискать какое-то другое решение, которое работает с Perspex, и я наткнулся на OmniXAML. Этот фремворк частично справился с поставленной задачей. Как его использовать с WPF, вы можете посмотреть здесь Using OmniXAML for WPF. Так как тема была использование Perspex, то настало самое время подвести итоги по данному фреймворку. В нем на данном этапе много недоработок и багов. Давайте пробежимся по тем, которые я смог найти, когда пытался тестировать этот фреймворк.
Дизайнер постоянно падает по самым разным причинам. Например, одной из самых популярных была невозможность задать имя для контрола.
Не работает байндинг на SelectedItem.
Не работает MultiBinding.
Байдинг работает по-разному для разных контролов. Аналогичная проблема уже зарегистрирована: The ElementName scope in Binding differs between TreeView and ListBox.
Не работает корректно меню и хоткеи на меню.
Нет возможности обновить явно команду (ReactiveCommand), так как основная проблема − что за классами она нормально не следит. Например, код с MainViewModel, который приведен ниже, не работает.
return _removeBookCommand ?? (_removeBookCommand =
    ReactiveCommand.CreateAsyncObservable(this.WhenAnyValue(
                x => x._selectedBook,
                x => CanRemoveBook(x)),
                RemoveBook));

Но вы сами можете перейти в метод CanRemoveBook и убедиться, что он вызывается всего один раз. Пока что единственный пример, который работает, я увидел у ребят на youtube.com. Несмотря на то что данный фреймворк в процессе разработки, я верю в успех этих ребят. Тем более что это выглядит очень здорово, и я хотел бы им пожелать, чтобы у них все получилось. Надеюсь, что через некоторое время часть ошибок будет исправлена, и мы сможем попробовать это фреймворк еще раз. В следующей статье мы рассмотрим использование фреймворка OmniXAML. Как минимум, OmniXAML сумел справиться с поставленной задачей, которая оказалась не по силам Perspex на 90%. (OmniXAML использует для своей работы Perspex). 

No comments:

Post a Comment