Saturday, December 19, 2015

OmniXAML – кросплатформенный XAML фреймворк

Здравствуйте, уважаемые читатели. Сегодня мы посмотрим в сторону кросплатформенного XAML фреймворка под названием OmniXAML. В прошлой статье мы рассматривали возможность написать на кросплатформенном фреймворке Perspex приложение чуть сложнее, чем набросок самых простых контролов. Чем это у меня закончилось, вы можете посмотреть здесь: "Как я Perspex пытался использовать с MVVM". В общем, все закончилось печально, и я так и не смог добить до конца примитивную задачу. Но тут мне на глаза попался другой фреймворк под названием OmniXAML, который связан с Perspex фреймворком, но который дает немного больше возможностей развернуться. Немного забегая наперед, скажу, что OmniXAML также не смог справиться с поставленной задачей на 100%, но в отличие от голого Perspex, OmniXAML дает больше возможностей для маневра. Чтобы не быть голословными, приступим к реализации. Давайте создадим новое приложение WPF Application, которое назовем “OmnimaxlSample”.
Следующим делом нам нужно установить с помощью NuGet Package Manager пакеты OmniXAML и OmniXaml.Wpf
Нам понадобятся оба пакета, потому что мы создали свое приложение как WPF Application, поэтому его нужно немного допилить, чтобы оно могло работать с OmniXAML. В отличие от Perspex, фреймворк OmniXAML уже вышел с бета-версии, и его можно спокойно использовать. После того как вы установите OmniXAML, обратите внимание на количество библиотек, которые вам будут загружены.
Их намного меньше, чем количество библиотек с того же Perspex, что мне очень нравится. Но хватит уже хвалить данный фреймворк; приступим к его реализации. Первым делом нам нужно проделать небольшую хитрость, чтобы избавиться от файла MainWindow.xaml.cs. Для этого нужно произвести все действия, которые описаны в инструкции. Если же английский язык для вас проблематичный, то ниже будет небольшой перевод.
Выбираем наш файл MainWindow.xaml и в окне свойств и выставляем свойство “Copy to Output Directory” в “Copy always”. Затем удаляем файл MainWindow.xaml.cs. Далее открываем файл MainWindow.xaml и заменяем его содержимое на следующее:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Title="MainWindow" Height="350" Width="525">
    <TextBlock Text="Hello World!" />
</Window>
Переходим в App.xaml и удаляем свойство StartupUri. После этого переходим в App.xaml.cs и переопределяем метод OnStartup, как показано ниже.
protected override void OnStartup(StartupEventArgs e)
{
    var xamlLoader = new WpfXamlLoader();

    Window mainWindow;
    using (var stream = new FileStream("MainWindow.xaml", FileMode.Open))
    {
        mainWindow = (Window)xamlLoader.Load(stream);
    }

    mainWindow.Show();
}
Добавляем namespace на библиотеку OmniXaml.Wpf
using OmniXaml.Wpf;
После этого собираем наш проект и запускаем.
P.S. На данном этапе у вас может случиться небольшое затруднение. Например, у меня Visual Studio 2015 категорически отказывалась копировать MainWindow.xaml в папку дебага. Пришлось для теста перенести это файл вручную.
Теперь после того как мы запустили наш простой пример, настало время немного его усложнить с использованием MVVM. Нам же нужно, чтобы пример хоть немного соответствовал реалиям.
В OmniXaml нет классов для работы с командами, которые будут нам нужны для того, чтобы обрабатывать нажатие на кнопки. Тут есть два выхода. Первый – написать свою реализацию, наследуясь от класса ICommand. Второй – для более ленивых, к которым отнесу себя, пожалуй, и я, – скачать какую-то готовую библиотеку, в которой это реализовано. Например, я для этих целей скачал себе Prism.Mvvm через NuGet Package Manager.
Самое время приступить к реализации. Добавим в наш проект следующую структуру папок: ViewModes, Views и Services.
Затем в папку 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; }
}
Затем в этой же папке добавим реализацию самой книги – класс Book.
public class Book : ViewModel, IBook
{
    private string _author;
    public string Author
    {
        get { return _author; }
        set
        {
            _author = value;
            OnPropertyChanged();
        }
    }

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

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

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

    private int _count;
    public int Count
    {
        get { return _count; }
        set
        {
            _count = value;
            OnPropertyChanged();
        }
    }
}
Здесь в качестве модели и наследовали наш класс от класса ViewModel (странное имя для класса, у которого единственное, что реализовано, – это метод OnPropertyChanged), для того чтобы у нас была доступна функция OnPropertyChanged.
Реализовываем интерфейс, который будет возвращать нам необходимые данные по книгам. Для этого в папку Services добавим интерфейс IBookService.
public interface IBookService
{
    void GetData(Action<ObservableCollection<IBook>, Exception> callback);
    IBook FindBook(IBook findBook);
    void CreateNewBook();
    void RemoveBook(IBook book);
}
В эту же папку и добавим реализацию, которая будет доставать нам все эти данные.
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
}
Как видите, этот интерфейс просто хардкодит некоторые данные, которые будут отправляться наружу. Теперь реализуем нашу модель представления. Она практично не меняется у меня с примера к примеру, и я уже подумываю, что нужно придумать какой-то другой тестовый пример, а не все время толкаться с книгами. Назовем нашу модель представления MainViewModel и добавим ее в папку ViewModels.
public class MainViewModel : ViewModel
{
    #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;
            });
    }
    #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
        {
            _selectedBook = value;
            OnPropertyChanged();
            RemoveBookCommand.RaiseCanExecuteChanged();
        }
    }
    #endregion

    #region Command
    private DelegateCommand _addBookCommand;
    public DelegateCommand AddBookCommand
    {
        get
        {
            return _addBookCommand ?? (_addBookCommand = new DelegateCommand(AddNewBook));
        }
    }

    private void AddNewBook()
    {
        _bookService.CreateNewBook();
    }

    private DelegateCommand _removeBookCommand;
    public DelegateCommand RemoveBookCommand
    {
        get
        {
            return _removeBookCommand ?? (_removeBookCommand = new DelegateCommand(RemoveBook, CanRemoveBook));
        }
    }

    private void RemoveBook()
    {
        Books.Remove(SelectedBook);
    }

    public bool CanRemoveBook()
    {
        return SelectedBook != null;
    }
    #endregion
}
Здесь также все на уровне примитивов. Сама модель представления только оперирует списком полученных книг, и в ней реализованы две команды: добавить новую книгу и удалить старую.
У нас осталась самая важная миссия — это реализация нашего представления. Поэтому открываем нашу форму MainWindow.xaml и правим код следующим образом:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <ListView Name="libary"  Grid.Column ="0" ItemsSource="{Binding Books}" SelectedItem="{Binding SelectedBook}">
            <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 SelectedBook}">
                <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>
В дизайнере это будет выглядеть вот так:
Тут немного пришлось подумать, как сделать так, чтобы оно все заработало. Детальный список, который вы видите с правой стороны, я обычно реализовываю следующим образом:
<Grid Grid.Row="0" DataContext="{Binding ElementName=libary, Path=SelectedItem}">
Но так как OmniXAML не хотел такую строку воспринимать и находить мой выбранный элемент, пришлось схитрить и написать вот так:
<Grid Grid.Row="0" DataContext="{Binding SelectedBook}">
Мы забыли еще один штрих – установить DataContext для главного окна. Переходим в класс App.xaml.cs и изменяем метод OnStartup следующим образом:
protected override void OnStartup(StartupEventArgs e)
{
    var xamlLoader = new WpfXamlLoader();

    Window mainWindow;
    using (var stream = new FileStream("MainWindow.xaml", FileMode.Open))
    {
        mainWindow = (Window)xamlLoader.Load(stream);
    }

    mainWindow.DataContext = new MainViewModel(new BookService());
    mainWindow.Show();
}
Мне не хотелось для такого простого примера ставить какой-то IoC контейнер ,поэтому получилось так, как вы видите в примере.
Теперь можно запустить наш пример и посмотреть, что все работает.

Итоги
Сегодня мы рассмотрели использование кросплатформенного фреймворка OmniXAML  фреймворка самого по себе интересного, но особых идей, как его и где использовать, мне не пришло; тем более, заявленная работа с Perspex не работает нормально. Из чистого любопытства я попробовал допилить кусок на Perspex. Возможно, со временем он действительно займет свою нишу в мире разработки ПО, а пока же пусть просто полежит в сторонке.

Исходники к статье вы можете скачать по ссылке: OmnimaxlSample.

No comments:

Post a Comment