Улучшаем биндинги в CSharpForMarkup

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Недавно мне пришлось разбираться с Xamarin Forms и на глаза попалась такая штука как CSharpForMarkup. Она показалась очень интересной, поскольку позволяет использовать стандарный C# вместо XAML, тем самым невилируя кучу неудобств связаных с XAML. Но реализация биндингов мне показался недостаточно хорошой. Поэтому я начал её улучшать при помощи expression-ов и Roslyn анализаторов. Кому интересно что с этого получилось прошу под кат.


Улучшаем CSharpForMarkup


Как я уже говорил CSharpForMarkup позволяет использовать стандарный C# вместо XAML. Если мы, например, захочем отобразить список элементов, то view для это будет иметь приблизительно следующий вид:


// ...

Content = new ListView()
    .Bind(ListView.ItemSourceProperty, nameof(ViewModel.Items))
    .Bind(ListView.ItemTemplateProperty, () => 
      new DataTemplate(() => new ViewCell 
        { 
          View = new Label { TextColor = Color.RoyalBlue }
                    .Bind(Label.TextProperty, nameof(ViewModel.Item.Text))
                    .TextCenterHorizontal()
                    .TextCenterVertical() 
         }))
// ...

Как по мне довольно простой и прямолинейный код, но уж больно много слов получается. Поскольку это обычный C#, это можно очень просто починить. Давайте скроем boilerplate код и оставим только, то что мы действительно хотим менять/видеть. Для это-то создадим статичный класс XamarinElements и определим в нем следующее:



public static class XamarinElements
{
    public static ListView ListView<T>(string path, Func<T, View> itemTemplate = null)
    {
        return new ListView
        {
            ItemTemplate = new DataTemplate(() => new ViewCell {View = itemTemplate?.Invoke()})
        }.Bind(ListView.ItemsSourceProperty, path);
    }
}

Дальше мы можем открыть XamarinElements через using static XamarinElements и использовать его вот так:


// ...
using static XamarinElements;
// ...

Content = ListView(nameof(ViewModel.Items), () => 
          new Label { TextColor = Color.RoyalBlue }
            .Bind(Label.TextProperty, nameof(ViewModel.Item.Text))
            .TextCenterHorizontal()
            .TextCenterVertical() 
         )
// ...

На мой взгляд стало намного лучше. Но мы все ещё используем nameof(), что имеет свои нюансы. Например, нету простого способа сделать "длиный" биндинг такой как 'Item.Date.Hour'. Чтобы его определить нужно будет конкатенировать строки, а это уже не удобно.


Кроме этого, у нас нету никакой зависимости между тем что мы передали в ListView и тем к какой модели мы биндим ItemTemplate. Т.е. если мы решим изменить содержимое ViewModel.Items, то ItemTemplate об этом никак не узнает и он может биндиться к тому, чего уже не существует.


Чтобы избежать этого мы можем использовать Expression<Func>. Это сразу упрощает построение длиных биндингов и позволит через джененрик установить связь между тем к какой колекции мы забиндились и тем к каким элементам мы будим биндиться. Новая реализация будет иметь следующий вид:


public static class XamarinElements
{
  public static ListView ListView<T>(Expression<Func<IEnumerable<T>>> path, Func<T, View> itemTemplate = null)
  {
      var pathFromExpression = path.GetBindingPath();
      return new ListView
      {
          ItemTemplate = new DataTemplate(() => new ViewCell {View = itemTemplate?.Invoke(default)})
      }.Bind(ListView.ItemsSourceProperty, pathFromExpression);
  }

  public static TView Bind<TView, T>(this TView view, BindableProperty property, Expression<Func<T>> expression)
    where TView: BindableObject
  {
      view.Bind(property, expression.GetBindingPath());
      return view;
  }
}

// ...
using static XamarinElements;
// ...

Content = ListView(() => ViewModel.Items, o => 
          new Label { TextColor = Color.RoyalBlue }
            .Bind(Label.TextProperty, () => o.Item.Date.Hour))
            .TextCenterHorizontal()
            .TextCenterVertical() 
         )
// ...

Обратите внимание что в itemTemplate мы передаем пустой инстанс элемента из колекции. Хоть он и пустой и обращаться к нему на прямую смысла нет вообще, но это позволяет нам использовать его при создании биндингов внутри ItemTemplate. Если содержимое колекции кардинально изменится, то биндинг сломается тоже. Но здесь есть своя ложка дегтя. Поскольку это Expression, то нам ничего не мешает написать следующее () => o.Item.Date.Hour + 1. С точки зрения компилятора все окей, но мы не можем сделать биндиг к такой штуке.


Но и тут не стоит отчаиваться. Нам на помощь приходит Roslyn с его анализаторами. Мы можем его попросить смотреть на все Expression-ы и если они используются в биндингах, но при этом нет возможности сгенерить адекватный биндинг, то пускай генерится ошибка компиляции. Так мы сразу узнаем что что-то пошло не по плану.


Пишем анализатор


Я не буду описывать как настроить проект для анализатора и как его тестировать. Это уже описано в моих предыдущих статьях. Желающие могут почитать их или посмотреть полный код анализатора в репозитории.


Сам анализатор получился очень простой:


// ...
 public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze |
                                                   GeneratedCodeAnalysisFlags.ReportDiagnostics);

            context.RegisterOperationAction(o => Execute(o), OperationKind.Invocation);
        }

        private void Execute(OperationAnalysisContext context)
        {
            if (context.Operation is IInvocationOperation invocation)
            {
                var bindingExpressionAttribute =
                    context.Compilation.GetTypeByMetadataName("BindingExpression.BindingExpressionAttribute");

                var methodWithBindingExpressions = invocation.TargetMethod.Parameters
                    .Any(o =>
                        o.GetAttributes()
                            .Any(oo => oo?.AttributeClass?.Equals(bindingExpressionAttribute) ?? false));

                if (!methodWithBindingExpressions)
                {
                    return;
                }

                foreach (var argument in invocation.Arguments)
                {
                    var parameter = argument.Parameter;
                    if (!parameter
                        .GetAttributes()
                        .Any(o => o?.AttributeClass?.Equals(bindingExpressionAttribute) ?? false))
                    {
                        continue;
                    }

                    if (argument.Syntax is ArgumentSyntax argumentSyntax &&
                        argumentSyntax.Expression is ParenthesizedLambdaExpressionSyntax lambda)
                    {
                        switch (lambda.ExpressionBody)
                        {
                            case MemberAccessExpressionSyntax memberAccessExpressionSyntax:
                                continue;
                            default:
                                context.ReportDiagnostic(
                                    Diagnostic.Create(BindingExpressionAnalyzerDescription,
                                        argumentSyntax.GetLocation()));
                                break;
                        }
                    }
                }
            }
        }

// ...

Всё что мы делаем это смотрим на вызовы методов и ищем там аргименты которые имеют аттрибут BindingExpression. Если такой аргумент есть, то смотрим состоит ли наш expression только из MemberAccessExpressionSyntax, если нет — генерим ошибку.


Финализируем


Что-бы заставить его работать в текущем примере нужно будет поставить nuget BindingExpression и немного подредактировать наш XamarinElements.


Обновленая версия имеет следующий вид:


public static class XamarinElements
{
  public static ListView ListView<T>(
    [BindingExpression]Expression<Func<IEnumerable<T>>> path,  // check this like
    Func<T, View> itemTemplate = null)
  {
      var pathFromExpression = path.GetBindingPath();
      return new ListView
      {
          ItemTemplate = new DataTemplate(() => new ViewCell {View = itemTemplate?.Invoke(default)})
      }.Bind(ListView.ItemsSourceProperty, pathFromExpression);
  }

  public static TView Bind<TView, T>(
    this TView view, BindableProperty property, 
    [BindingExpression]Expression<Func<T>> expression) // check this like
    where TView: BindableObject
  {
      view.Bind(property, expression.GetBindingPath());
      return view;
  }
}

После чего следующий пример уже не скомпилируется:


// ...
using static XamarinElements;
// ...

Content = ListView(() => ViewModel.Items, o => 
          new Label { TextColor = Color.RoyalBlue }
            .Bind(Label.TextProperty, () => o.Item.Date.Hour + 1)) // error here
            .TextCenterHorizontal()
            .TextCenterVertical() 
         )
// ...

Вот таким относительно простым в использовани способом можно упростить и обезопасить написание xamarin приложений с использованием CSharpForMarkup.


На этом думаю всё. Пожелания и идеи преветствуются.


Исходники анализатора лежат тут: GitHub

Источник: https://habr.com/ru/post/551768/


Интересные статьи

Интересные статьи

Привет, Хабр! Пользователям смартфонов HUAWEI и HONOR по умолчанию доступно большое количество режимов и эффектов съёмки: ночная съёмка, распознавание сцен, HDR, широкая диафрагма и т. д....
В этой статье мы рассмотрим, как система управления 1С-Битрикс справляется с большими нагрузками. Данный вопрос особенно актуален сегодня, когда электронная торговля начинает конкурировать по обороту ...
Возможность интеграции с «1С» — это ключевое преимущество «1С-Битрикс» для всех, кто профессионально занимается продажами в интернете, особенно для масштабных интернет-магазинов.
Один из самых острых вопросов при разработке на Битрикс - это миграции базы данных. Какие же способы облегчить эту задачу есть на данный момент?
Согласно многочисленным исследованиям поведения пользователей на сайте, порядка 25% посетителей покидают ресурс, если страница грузится более 4 секунд.