Home .NET Improving bindings in CSharpForMarkup

Improving bindings in CSharpForMarkup

by admin

I recently had to deal with Xamarin Forms and I came across such a thing as CSharpForMarkup I found it very interesting, because it allows to use standard C# instead of XAML, counterbalancing a lot of XAML-related inconveniences. But the bindings implementation didn’t seem good enough to me. That’s why I began to improve it with the help of expressions and Roslyn parsers. If you want to know what I’ve got here, please follow this link.

Improving CSharpForMarkup

As I said before CSharpForMarkup allows to use standard C# instead of XAML. If, for example, we want to display a list of items, the view for this will look something like this

// ...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()}))// ...

Seems pretty simple and straightforward code to me, but it’s a lot of words. Since it’s regular C#, this can be fixed very easily. Let’s hide the boilerplate code and leave only what we really want to change/see. To do this, let’s create a static class XamarinElements and define in it the following :

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);}}

Next we can open XamarinElements via using static XamarinElements and use it like this :

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

It’s gotten a lot better in my opinion. But we still use nameof() which has its own nuances. For example, there is no easy way to make "long" bindings like ‘Item.Date.Hour’. To determine it, you have to concatenate strings, and that’s not convenient.

Besides, we don’t have any relation between what we pass to ListView and what model we bind ItemTemplate to. So, if we decide to change the content of ViewModel.Items then ItemTemplate does not know about this in any way, and it can bind to something that does not already exist.

To avoid this we can use Expression<Func> . This immediately simplifies the construction of long bindings and allows us to make a connection between which collection we are binded to and which elements we are going to bind to. The new implementation will look like this

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())// ...

Note that we pass an empty instance of an item from the collection to the itemTemplate. Even though it’s empty and there’s no reason to refer to it directly, this allows us to use it to create bindings inside the ItemTemplate. If the content of the collection changes drastically, the bindings will also break. But this has its own fly in the ointment. Since this is an Expression, nothing prevents us from writing the following () => o.Item.Date.Hour + 1. From the compiler’s point of view it’s OK, but we can’t bind to such a thing.

But don’t despair here either. Roslyn and his analyzers come to our aid. We may ask him to look at all the Expressiones and if they are used in the bindings and there is no way to generate an adequate bindings, let it generate a compilation error. That way we’ll know right away that something has gone wrong.

Let’s write an analyzer

I will not describe how to set up a project for the analyzer and how to test it. It is already described in my previous articles. If you want, you may read them or look through the full analyzer’s code in the repository.

The analyzer itself is very simple :

// ...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;}}}}}// ...

All we do is look at method calls and look for arguments that have the attribute BindingExpression If there is such an argument, then we see if our expression consists only of MemberAccessExpressionSyntax, if not, we generate an error.

Finalize

To make it work in the current example you would have to put nuget BindingExpression and some tweaking of our XamarinElements

The updated version is as follows :

public static class XamarinElements{public static ListView ListView<T> ([BindingExpression]Expression<Func<IEnumerable<T> > > path, // check this likeFunc<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 likewhere TView: BindableObject{view.Bind(property, expression.GetBindingPath());return view;}}

After that the next example will not compile anymore :

// ...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())// ...

This is a relatively easy to use way to simplify and secure writing xamarin applications using CSharpForMarkup.

I guess this is it. Suggestions and ideas are welcome.

The analyzer sources are here : GitHub

You may also like