Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Spec] CSharpForMarkup #8342

Open
wants to merge 8 commits into
base: master
from
Open

[Spec] CSharpForMarkup #8342

wants to merge 8 commits into from

Conversation

@VincentH-Net
Copy link

VincentH-Net commented Oct 31, 2019

CSharpForMarkup

Fluent helpers and classes to make building Xamarin Forms UI in declarative style C# a joy.

Modern UI frameworks such as Flutter and SwiftUI offer declarative UI.
These helpers offer a similar approach for Xamarin Forms (the UI pattern remains MVVM though, this feature does not add MVU).

The goal is to make Xamarin Forms more attractive for additional developer audiences that don't have XAML skills / preference. Developers with XAML skills can also benefit; declarative C# can be very similar to XAML and can offer increased productivity (due to excellent IDE support for C# and no need for language bridging mechanisms).

This feature is based on CSharpForMarkup (174 stars atm).
This PR code has been tested against this Production App by replacing CSharpForMarkup with it.

API

The API is an opt-in set of fluent helpers and classes that extend Element, View, VisualElement, Layout, VisualElement, Style and some specific view types. No existing Forms code is touched. There is no impact on existing apps code.

Developers opt in by adding these usings to a markup .cs file:

using Xamarin.Forms.Markup;
using static Xamarin.Forms.Markup.EnumsForGridRowsAndColumns; // Optional

The separate Markup namespace promotes a clean separation between UI markup and UI logic. It is recommended to split C# markup and UI logic into separate partial class files, e.g:

  • LoginPage.cs
  • LoginPage.logic.cs

The helpers offer a fluent API with Bind, Effects, Invoke, Assign, Row, Col, FormattedText, Style, Font, inline converters, support for using enums for Grid rows + columns and more.
Simple to add to in app code; developers can also easily create their own markup DSL on top of this,
as in this example in David Ortinau's Xappy.

C# Examples

Just an Entry with Style, Row, Margins and Bind:

new Entry { Placeholder = "123456", Keyboard = Keyboard.Numeric } .Style (Styles.FieldEntry)
.Row (Row.CodeEntry) .Margins (left: entryAlignLeftMarginSize)
.Bind (nameof(vm.RegistrationCode))

A few more layout helpers ColSpan, Right, FillExpandH, CenterV, Bottom, Margin, Height, TextCenterH:
PageCSharpShort

Methods

Extension methods to set select view properties. The helpers are not meant to replace all property setters; they are added when they improve readability. They are meant to be used in combination with normal property setters. It is recommended to always use a helper when one exists for a property, but developers can choose a balance they prefer (e.g. to keep the markup more or less similar to XAML).

Binding and converters

Use Bind as in the above C# examples.

Note that Bind knows the default bindable property for most view types; you can omit it in most cases. You can also register the default bindable property for additional controls:

DefaultBindableProperties.Register(
    HoverButton.CommandProperty, 
    RadialGauge.ValueProperty
);

You can specify any bindable property like this:

new Label { Text = "No data available" }
.Bind (Label.IsVisibleProperty, nameof(vm.IsEmpty))

Bind a command to any type of view using gesture recognizers:

new Label { Text = "Tap Me" }
.BindTapGesture (nameof(vm.TapCommand)) // Or use any type of gesture with Bind<TView, TGestureRecognizer>(...)

Pass inline converter code with convert and convertBack parameters (type-safe):

new Label { Text = "Tree" }
.Bind (Label.MarginProperty, nameof(TreeNode.TreeDepth), convert: (int depth) => new Thickness(depth * 20, 0, 0, 0))

Re-use converter code and instances with FuncConverter:

treeMarginConverter = new FuncConverter<int, Thickness>(depth => new Thickness(depth * 20, 0, 0, 0));
//...
new Label { Text = "Tree" }
.Bind(Label.MarginProperty, nameof(TreeNode.TreeDepth), converter: treeMarginConverter),

Use FormattedText together with binding to Spans:

new Label { } .FormattedText (
    new Span { Text = "Built with " },
    new Span { TextColor = Color.Blue, TextDecorations = TextDecorations.Underline }
    .BindTap (nameof(vm.ContinueToCSharpForMarkupCommand))
    .Bind (nameof(vm.Title))
)

Note that you can bind gestures to spans with BindTap and BindGesture (due to C#'s inability to have generic overloads with different where clauses these have to be named different from the helpers for Views).

Layout

Use layout helpers for positioning views in layouts and content in views:

  • In a Grid: Row, Col, RowSpan, ColSpan
  • In a FlexLayout: AlignSelf, Basis, Grow, Menu, Order, Shrink
  • Specify LayoutOptions:
    • Left, CenterH, FillH, Right
    • LeftExpand, CenterExpandH, FillExpandH, RightExpand
    • Top, Bottom, CenterV, FillV
    • TopExpand, BottomExpand, CenterExpandV, FillExpandV
    • Center, Fill, CenterExpand, FillExpand
  • Margin, Margins
  • Height, Width, MinHeight, MinWidth, Size, MinSize
  • Padding, Paddings
  • In a Label:
    • TextLeft, TextCenterH, TextRight
    • TextTop, TextCenterV, TextBottom
    • TextCenter

The recommended convention is to put all helpers from above set for a view on a single line, in the order that they are listed above. This creates a layout line that visually zooms in on the view content:

  1. Row & col that contain the view
  2. Alignment within row & col
  3. Margin around view
  4. View size
  5. Padding within view
  6. Content alignment within padding

Consistently applying this convention enables developers to quickly visually scan and zoom in on markup to build a mental map of where the view content is located.

Enums for Grid rows and columns

By adding using static Xamarin.Forms.Markup.EnumsForGridRowsAndColumns; developers can use enums for Grid rows and columns instead of numbers, so they don't have to renumber manually when they add or remove rows or columns. Readability and intent of the layout is also improved:

Enums For Grid Rows And Columns

Fonts

  • In a Label: FontSize, Bold, Italic
  • In Button, Label, Entry, Picker: Font

Effects

new Button { Text = "Tap Me" }
.Effects (new ButtonMixedCaps())

Logic integration

Use Invoke to execute code inline in your declarative markup:

new ListView { } .Invoke (l => l.ItemTapped += MyListView_ItemTapped)

Use Assign if you need to access a control from outside the UI markup (in UI logic):

new ListView { } .Assign (out MyListView),

Styles

Use Style to create type-safe, declarative coded styles:

using static CSharpForMarkupExample.Styles;
...
new Button { Text = "Tap Me" } .Style (FilledButton), // Use .Style for VisualElement types
...
new Span { Text = "delightful", Style = Quote }, // Use the implicit Style type converter for other Element types

Styles In C Sharp

Note that instead of using Style, you can simply create your own extension methods to set styling even more type-safe:

new Button { Text = "Tap Me" } .Filled ()
public static TButton Filled<TButton>(this TButton button) where TButton : Button
{
    button.Buttons(); // Equivalent to Style .BasedOn (Buttons)
    button.TextColor = Color.White;
    button.BackgroundColor = Color.Red;
    return button;
}

Platform Specifics

There is also support for some Platform Specifics e.g. iOSSetGroupHeaderStyleGrouped.
Using these helpers avoids the name conflicts on view types that you get when using platform specifics from the Xamarin Forms namespaces.

Conventions

Recommended markup formatting conventions for every control:

  • Bound properties are last, one per line. The default bindable property, if any, should be the very last line.

  • The line before the bound properties is about layout, ordered inward: rows / cols, layout options, margin, size, padding, content alignment.

    Consistently applying these conventions allows to visually scan / zoom markup and build a mental map of the layout.

  • Before that are the other properties; any property that identifies the control's purpose should be on the very first line (e.g. Text or Placeholder)

Backward Compatibility

There are no breaking changes. All platforms are supported.

Area's where functionality still can be added:

  • Add bindable property default for more/all built-in Xamarin Forms controls
  • Add Shell helpers
  • Add helpers for more/all platform specifics

Difficulty : [low]

Because it is fairly complete and thoroughly battle-tested.

Platforms Affected

  • Core (all platforms)

Behavioral/Visual Changes

None

Testing Procedure

This PR code has been tested against this Production App by replacing CSharpForMarkup with it.

If preferred, some example pages can be added to the PagesGallery project for testing.

Vincent Hoogendoorn added 6 commits Oct 31, 2019
…Markup/blob/061566282706de22d040e5b7b32bc3c2e3e323e3/src/XamarinFormsMarkupExtensions.cs

- Remove specific converters
- Fix .NETStandard 1 build errors by refactoring IConvertible to Enum generic constraints
- Add RegisterDefaultBindableProperties method
- Reformat source to comply with Forms contibuting guidelines
… impact on existing code and to support clear separation of UI markup from UI logic

- Split up source file for better maintainability
- Names of generic types and variables to reflect where clauses
- Namespaces remove unused & sort
- Add whitespace
- Remove unused file
Remove RowCol helpers because compiler cannot infer its generic type parameters. Use Row and Col methods instead
@VincentH-Net VincentH-Net requested a review from StephaneDelcroix as a code owner Oct 31, 2019
@dnfclas

This comment has been minimized.

Copy link

dnfclas commented Oct 31, 2019

CLA assistant check
All CLA requirements met.

@rogihee

This comment has been minimized.

Copy link
Contributor

rogihee commented Nov 1, 2019

I'm getting a full-circle feeling from this, after starting my Xamarin journey with @migueldeicaza https://github.com/migueldeicaza/MonoTouch.Dialog in 2013 (look at the sample code in the README).

Shameless plug for an MVU prototype: anyone interested should checkout @Clancey https://github.com/Clancey/Comet

@samhouts samhouts self-requested a review Nov 1, 2019
@samhouts samhouts added this to In Review in vNext (master) Nov 2, 2019
@VincentH-Net

This comment has been minimized.

Copy link
Author

VincentH-Net commented Nov 2, 2019

I just updated the spec doc to include all helpers and explain intended usage

@VincentH-Net

This comment has been minimized.

Copy link
Author

VincentH-Net commented Nov 15, 2019

Maintainability and Codegen

During yesterday's discussion of this PR in the XamarinFormTeam live stream some concerns were mentioned about maintainability (there was great positive feedback from the team and the community chat as well, thanks all!). For maintainability a possible alternative approach was proposed: automatically generate a full fluent API.

I would like to address these concerns by demonstrating:

  • How maintaining the current partial & manual Fluent approach requires very little effort
  • How using codegen to automatically create a full fluent UI API would not improve C# markup readability or conciseness - which is the whole point of this PR.

Light Maintenance

I have been maintaining CSharpForMarkup for over 2 years and it has been very little work. To illustrate, in 2019 only 6 small commits were done to the helpers (note that in this PR the single helpers file already has been split up in multiple files for optimal maintainability)

When new UI will be added to Forms - e.g. a new control or a feature like Shell, there is only a need to add helpers for those properties (or property combinations) that are overly verbose and most often used. A helper is essentialy just one assignment statement, or a combination of helpers:

public static TLabel TextTop<TLabel>(this TLabel label) where TLabel : Label 
{
    label.VerticalTextAlignment = TextAlignment.Start; 
    return label; 
}

public static TLabel TextCenter<TLabel>(this TLabel label) where TLabel : Label => label.TextCenterH().TextCenterV();

This allready small effort can be even further reduced: initially a minimal amount of helpers can be added for a new control/feature. After a period where the control is used in apps, app developers will have added common convenience methods (it is trivially easy as shown above) which can be simply contributed in a PR. It can be a process of small steps for gradual-but optimal- improvement, which would grow a standard body of helpers.

The vast majority of existing helpers are for base classes (Element, View, VisualElement, Layout) and for layout classes that provide attached properties (Grid, FlexLayout). Additions to Forms of this type are relatively rare and even then would only require a handful of very similar methods.

The PR only accesses public Forms properties and methods. These are highly unlikely to change but even if they were, e.g. a rename would automatically be done since the PR is in the Core project (which is a benefit as compared to putting it in a separate NuGet). So there is no risk of changes in Forms causing maintenane in this PR.

The implementation in this PR has been used during more than 2 years in production apps; it is battle tested for stability and usability. It is already documented. All this value can be included in Forms for free.

In summary, future maintainance for the current implementation is very light.

CodeGen API would have little value

While automatically generating a full fluent API would prevent the above (light) maintenance, it would not improve much for the developer. C# markup is improved by intelligently choosing shorter names for the most often used (combinations of) overly verbose property setters. Simply automatically converting the vanilla Forms UI API to equivalent fluent methods would:

  • improve little compared to vanilla Forms C# UI
  • pollute IntelliSense
  • add much more code to apps

The codegen Fluent api approach has already been tried e.g. here is the most starred (17 stars) fluent Xamarin UI API:
https://github.com/dariodotnet/XamFluentUI
Note that CSharpForMarkup currently has 178 stars:
https://github.com/VincentH-Net/CSharpForMarkup

To illustrate why, compare below C# markup fragment in 4 different API's:

// Vanilla Forms:
new Label { Text = "Hi", HorizontalTextAlignment = TextAlignment.Center, VerticalTextAlignment = TextAlignment.Center },

// Generated 1:
new Label("Hi", horizontalTextAlignment: TextAlignment.Center, verticalTextAlignment: TextAlignment.Center),

 // Generated fluent 2:
new Label("Hi") .HorizontalTextAlignment(TextAlignment.Center) .VerticalTextAlignment (TextAlignment.Center),

// CSharpForMarkup:
new Label { Text = "Hi" } .TextCenter (),

A fully fluent API is not a goal, it is only a means to be used selectively and intelligently where it improves on the readability and conciseness of vanilla Forms C# markup.

As can be seen in above examples, no real improvement is achieved when automatically generating an API. Of course you can manually add attributes for the codegen, but that would mean equivalent or more effort and more complexity.

In short, building a fluent UI API + generator would be a lot of extra effort for a lot less value.

Getting it shipped

If the concerns are adequately addressed, possibly add an experimental flag to communicate that syntax may still be finetuned for a time, and it can be shipped!

PS Follow up possibilities:

  • We could start to write all new C# UI documentation examples in this style (OK because the current PR is in the Core project, something that may not be acceptable if it would be a separate NuGet).
  • Devs may also like a VS template in this style.
  • I've been thinking about creating an autoformatting tool/plugins ; experience has tought that markup C# readability improves with some different formatting options than for logic C#, and also automate more conventions (see conventions in PR description)

Thank you all for reading all this; please like if you agree or comment if you have additional concerns or questions. I believe that doing this right is important for the future of Forms (attracting more devs). Let's make this happen!

@davidortinau @samhouts @StephaneDelcroix

@muhaym

This comment has been minimized.

Copy link

muhaym commented Nov 16, 2019

@VincentH-Net I would love to see a performance comparison for pages created using XAML vs C#
Complex Page Rendering results with performance insights will help developers to choose the way to go.

For instance, I have a very very complex page that has lot's of sub controls, which based on dynamic condition should be loaded, right now since Conditional XAML is not available, we use Visibility to meet the needs, but as the page grown in size and complexity, the page loading is severely slow (takes 1-2 seconds for rendering the UI in good phones).

@KSemenenko

This comment has been minimized.

Copy link
Contributor

KSemenenko commented Nov 16, 2019

I like this feature. Sometimes I need to make page layout from code, and now this is not very convenient.

@VincentH-Net

This comment has been minimized.

Copy link
Author

VincentH-Net commented Nov 16, 2019

@muhaym CSharpForMarkup is by definition faster than XAML, even when compared to XAML with precompiled XAML and compiled bindings. At it's core XAML is just an object serialization format, built on top of the UI API that you directly access from C# when you use CSharpForMarkup. You simply bypass the XAML layer, and the compiler optimizes your C# markup better than XAML.

Also when using CSharpForMarkup you don't need to use any XAML to C# language bridging mechanisms (e.g. styles, resource dictionaries, behaviours, triggers, markup extensions), some of which can also be slowdowns.

I have used CSharpForMarkup to build a production app with some quite complex pages. I never experienced the type of slowness you describe. Actually people watching my video walkthrough asked if I speeded it up (I didn't).

You can check for yourself; download my app and select "or start demo" - you can navigate the full app UI with demo data to check the speed.

You can check the screen below, I use the same approach there that you do (all controls on the page, set visibility on the top tab select). I never needed to create the controls dynamically; it was already fast enough.

image

@Depechie

This comment has been minimized.

Copy link
Contributor

Depechie commented Nov 16, 2019

Just for reference, since xamarin forms does UWP. Not all code is faster!
https://twitter.com/harini_kannan/status/994259239802617856?s=21

@SuNNjek

This comment has been minimized.

Copy link
Contributor

SuNNjek commented Nov 20, 2019

@Depechie Don't the view renderers instantiate its controls via code anyway? Xamarin doesn't use UWP Xaml, so this shouldn't matter

@Depechie

This comment has been minimized.

Copy link
Contributor

Depechie commented Nov 20, 2019

@SuNNjek yeah, that was my point exactly. But the statement that coded UI is faster is floating around and if you should do 'native' UWP dev, that statement is not true. But indeed because of the Xamarin Forms framework we can't benefit from that speed gain.

@KSemenenko

This comment has been minimized.

Copy link
Contributor

KSemenenko commented Nov 20, 2019

I also wanted to know, will it code support the Shell?

@VincentH-Net

This comment has been minimized.

Copy link
Author

VincentH-Net commented Nov 21, 2019

@KSemenenko Yes, I have Shell support planned, see the PR description:

Area's where functionality still can be added:

  • Add bindable property default for more/all built-in Xamarin Forms controls
  • Add Shell helpers
  • Add helpers for more/all platform specifics

I might even get it included before review of this PR starts (it has been 3 weeks now, no idea how long that takes though).

Do you have some example pain points of working with Shell in C#?
I can make sure to address them. Thanks!

@KSemenenko

This comment has been minimized.

Copy link
Contributor

KSemenenko commented Nov 21, 2019

Today I tried to create a Shell from the code.
minimal example:

var shell = new Shell();
shell.Items.Add(new TabBar()
{
	Items = { new Tab
	{
		Items = { new ShellContent()
		{
			Content = new StatusBarGalleryPage()
		}}
	}}
});

Application.Current.MainPage = shell;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
vNext (master)
  
In Review
9 participants
You can’t perform that action at this time.