Здравствуйте,
уважаемые читатели. Сегодня я немного расскажу о таком проекте, как 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