[.NET, C#, Xamarin] Улучшаем биндинги в CSharpForMarkup
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Недавно мне пришлось разбираться с 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
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка игр, C#, Unity] Разработка своей Just Shapes & Beats и как всё началось
- [Разработка игр, C#, Unity] Синтезатор на Unity 3D
- [Разработка веб-сайтов, .NET, ASP, C#, Микросервисы] Учим ASP.NET Core новым трюкам на примере Json Rpc 2.0
- [Системное администрирование, .NET, DevOps] Обновление процесса CI/CD: год спустя
- [Java, .NET] «Microsoft Coffee»: первоапрельский ответ на Java
- [.NET, C#] Страсти по Serilog + .NET Core: Глобальный логгер
- [Разработка игр, C#, Прототипирование, Дизайн игр, Игры и игровые приставки] Tantramantra и магия проектирования
- [.NET, Xamarin, Интервью] Интервью с Мигелем де Икасой: Microsoft, Mono, смартфоны и многое другое
- [.NET, C#, Разработка под Windows] Делаем откаты БД в msi. История про создание резервных копий и удаление БД в WixSharp
- [Open source, .NET, XML, C#] Конвертируем ODT в XML
Теги для поиска: #_.net, #_c#, #_xamarin, #_c#, #_xamarin, #_xamarin.forms, #_roslyn_analyzers, #_csharpformarkup, #_.net, #_c#, #_xamarin
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:59
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Недавно мне пришлось разбираться с 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 =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:59
Часовой пояс: UTC + 5