maui
maui copied to clipboard
Simplification of user interface declaration in code
Description of Change
It adds the ability to declare content inside curly braces.
This PR:
- implements the
IEnumerableinterface for classes that do not already have theIListinterface implemented and have the[ContentProperty]attribute. - adds a sample page -
FluentExamplePage.cswith sample extension methods (Maui.Controls.Sample/Pages/Fluent) and places it in the "Others" section of the sample application - implements unit tests in
ContentPropertyUnitTest.csfile to test the content property attribute and implementation of theIEnumerableinterface - adds missing content property attributes for classes
[ContentProperty(nameof(Points))]
public sealed partial class Polyline { ... }
[ContentProperty(nameof(Data))]
public sealed partial class Path { ... }
[ContentProperty(nameof(Points))]
public sealed partial class Polygon { ... }
[ContentProperty(nameof(Content))]
public partial class RadioButton { ... }
[ContentProperty(nameof(Setters))]
public sealed class VisualState { ... }
Usage example:
sample extension methods are declared in the Maui.Controls.Sample application folder /Pages/Fluent/Extensions
new ScrollView
{
new VerticalStackLayout
{
new Label()
.Text("Fluent API Page")
.FontSize(50)
.TextColor(ExampleStyleResources.PrimaryColor)
.HorizontalOptions(LayoutOptions.Center),
new Border
{
new Grid
{
new Label()
.Text("I'm .NET Bot")
.TextColor(Colors.LightGray)
.FontSize(40)
.HorizontalOptions(LayoutOptions.Center)
.VerticalOptions(LayoutOptions.Center),
new Image()
.Row(1)
.Source("dotnet_bot.png"),
new Label()
.Text("Hello, World!")
.Row(2)
.TextColor(Colors.LightGray)
.FontSize(30)
.Margin(new Thickness(10))
.HorizontalOptions(LayoutOptions.Center),
new Button()
.Text("Click me")
.Row(3)
.WidthRequest(220)
.HorizontalOptions(LayoutOptions.Center)
.Margin(new Thickness(40))
.OnClicked(button =>
{
count++;
button.Text = $"Clicked {count} ";
button.Text += count == 1 ? "time" : "times";
})
}
.RowDefinitions(e => e.Star().Star(2).Auto().Auto())
}
.StrokeShape(new RoundRectangle().CornerRadius(30))
.BackgroundColor(Colors.DarkSlateGrey)
}
}
Issues Fixed
Fixes #12678
Hey there @idexus! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.
/azp run
Azure Pipelines successfully started running 2 pipeline(s).
Thanks for the PR. Still going over this and will need some discussion. I think generally it is a sound addition but not sure what the team thinks. Also, we may wish to make sure the various community toolkit peoples get a say so we don't add something that gets in the way of existing things.
This is a personal thing, but I see that the view is constructed with the content first and then the properties:
var border = new Border
{
new Button()
.Text("Hello")
}
.BorderColor(Red);
Once the views get deep, the properties may get lost. Is there way to get more fluent without separating properties and instances?
var border = new Border()
.BorderColor(Red)
.Content(
new Button()
.Text("Hello")
)
This is just preference, but what is the advantage of having the initializers and properties after as opposed to an alternate?
When using the IDE, vertical lines help you when using curly braces, but not when using round braces, and honestly with more complex ones it gets unreadable.
vs
The solution for this could be add constructors
public Border(System.Action<Border> configure)
{
configure(this);
}
then you could
I did something like this in my library with extra constructors for easy inline assignment
public Slider(out Slider slider)
{
slider = this;
}
then you can do:
this.Content = new VerticalStackLayout
{
new Slider(out var slider)
.Minimum(1)
.Maximum(20),
new Label()
.Text(e => e.Path("Value").Source(slider).StringFormat("Value: {0}"))
.FontSize(28)
.TextColor(Colors.Blue)
};
If there is such a decision, I can also prepare a PR with the constructors.
@brminnick this seems something in your area of expertise/liking. Any thoughts?
@brminnick this seems something in your area of expertise/liking. Any thoughts?
Thanks for looping me in! I think this is a great first step for improving the out-of-the-box experience for devs for prefer to use C# (no XAML) for creating .NET MAUI UIs 💯
Not related to this PR, but I'd it if we also overloaded the constructors too to allow devs to set any Property in-line 👇
Overloaded Constructors with Default Parameters
Overloaded Constructors with Default Parameters
Add a constructor accepting every Property as a default/optional parameter.
Implementation
Since every control is already a partial class, I recommend using source generators to create a constructor that accepts every Property as a default parameter
- Use Source Generators to create a new Constructor that accepts every Property as a default parameter
- In the constructor, assign the Properties from their appropriate parameters
Source Generator Output
namespace Microsoft.Maui.Controls;
public partial class Button
{
public Button(Thickness padding = default,
LineBreakMode lineBreakMode = default,
Thickness margin = default
// continue adding a parameter for every Property of `Button`, including the properties inherited by `View`
)
{
this.Padding = padding;
this.LineBreakMode = lineBreakMode;
this.Margin = margin;
// continuing assigning every parameter to its Property
}
}
Example Usage
Content = new Button(text = "Hello World", textColor = Colors.Green);
@brminnick Thanks for your support on this topic 💯 :) You said it right, need better out-of-the-box support directly in the maui project for people who are far from XAML. Creating an interface in code gives you much more possibilities, and with some improvements, sometimes cosmetic, it can become the number one choice for creating applications in MAUI.
As for your proposal and overloading the constructors with all properties with default values, it will create some performance problem, because if you want to set one or two properties, all will be assigned (for only Button and View -> there are 21 bindable properties). Here, it would be better to have compiler support, in which it would be possible to write extension methods omitting e => e
instead of
new Border(e => e
.StrokeShape(new RoundRectangle().CornerRadius(10))
.Stroke(e => e.Path(nameof(BorderColor))) // <= using parameters you can't do this
.BackgroundColor(e => e.Path(nameof(CardColor)))
.SizeRequest(220, 350) // <= using parameters you can't do that either
.Margin(50)
.Padding(30))
{
new Slider(out var slider)
.Minimum(1)
.Maximum(20),
new Label()
.Text(e => e.Path("Value").Source(slider).StringFormat("Value: {0}"))
.FontSize(28)
.TextColor(Colors.Blue)
}
you could write (but it's a song of the future ;)
new Border(
.StrokeShape(new RoundRectangle().CornerRadius(10))
.Stroke(.Path(nameof(BorderColor)))
.BackgroundColor(.Path(nameof(CardColor)))
.SizeRequest(220, 350)
.Margin(50)
.Padding(30))
{
new Slider(out var slider)
.Minimum(1)
.Maximum(20),
new Label()
.Text(.Path("Value").Source(slider).StringFormat("Value: {0}"))
.FontSize(28)
.TextColor(Colors.Blue)
}
Anyway, I think adding constructors that can use extension methods has more power (like in LINQ), and many of ext. methods are in your CommuityToolkit Markup now. If you add the possibility of in-line property binding to this, creating a UI in the code will become very user-friendly.
@mattleibow This approach will also make it easier to build user interface with HotReload support for the MVVM pattern, only in the code.
This is an example from my library which is a wrapper on top of yours
@mattleibow @jfversluis Are there any decisions regarding this PR?
@brminnick @VincentH-Net any thoughts either way? Not sure if @PureWeen has thoughts after doing fabulous things...
I'll give the code a re-review and see what changed. But I think this as the concept is good to merge.
All I need is a few approvals from other code based things so we don't break them.
@brminnick @VincentH-Net any thoughts either way?
Thanks for involving me - I'm glad to see MAUI improvements for developers using C# for markup!
My 2cts on above discussion:
-
Imo the ability to declare content inside curly braces is a step in the right direction. In C# Markup 2, this is achieved by making child controls
paramsof a parent control constructor, so it uses parentheses (since C# Markup 2 eliminates the need fornew, it cannot use the curly braces approach). Curly braces have the (slight) benefit over parentheses of having guidelines and being collapsible in the editor without requiring an editor extension. Note that given the next point this is not a big plus, since functions have guidelines as well. -
Children being declared before properties in a parent is in my experience not an issue in real world applications: users of C# markup tend to break up the markup into smaller functions long before it gets unwieldy (so it reads like a story, top-down), E.g.
StackPanel Tweet() => VStack( TweetHeader(), TweetBody(), TweetFooter() );In general, devs tend to structure C# markup more finely than equivalent XAML because of the ease and many options of C# to do so. Another difference is styles - they tend to be used less because C# has more powerful & dev friendly alternatives for reuse.
-
Not for this PR, but on adding constructors that take property values (I assume with codegen?):
- There is no inheritance, so you would have to duplicate that code for every property in all ancestors, leading to a many times bigger API implementation size. The bulk of properties is defined in base classes.
- C# Markup 2 only generates ctor parameters for the properties defined in the class itself, not for inherited properties (unless codegen parameters instruct to do that for specific base classes or parameters). In addition it defines convenience ctor overloads, with only the most commonly used properties. This corresponds to SwiftUI concept of important properties. Finally, only parameters that have a non-default value are assigned, which reduces the performance impact.
@mattleibow @PureWeen @hartez @jfversluis @davidortinau
To convince you, in XAML such a solution is rather not possible, in the code it is. Example from my lib:
using
public static void Add<T>(this T list, Action<IList<IView>> itemsBuilder)
where T : IList<IView>
{
List<IView> items = new List<IView>();
itemsBuilder(items);
foreach (var item in items)
list.Add(item);
}
you get this:
@mattleibow @PureWeen @hartez @jfversluis @davidortinau
Are there any decisions regarding the addition of the IEnumerable interface to classes in the next release?
Other examples of use
Using IEnumerable interface, there is also no need to create constructors to put properties at the top, and more.
You can implement extension Add methods like these:
public static void Add<T>(this T obj, Func<T, T> configure)
where T : IEnumerable
{
configure(obj);
}
public static void Add<T>(this T layout, IEnumerable<View> items)
where T : Layout
{
foreach (var item in items)
layout.Children.Add(item);
}
and you get
- properties declaration inside the body
- even support for LINQ inside the body
public class KeypadPage : ContentPage
{
string[] labels = new[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#" };
public KeypadPage(KeypadViewModel vm)
{
BindingContext = vm;
Content = new Grid
{
// ---- properties ----
e => e
.RowDefinitions(e => e.Auto(count: 5))
.ColumnDefinitions(e => e.Absolute(100, count: 3))
.HorizontalOptions(LayoutOptions.Center)
.VerticalOptions(LayoutOptions.Center)
.ColumnSpacing(10)
.RowSpacing(10),
// ---- content here ----
new Label()
.ColumnSpan(2)
.Text(e => e.Path("DisplayText"))
.LineBreakMode(LineBreakMode.HeadTruncation)
.VerticalTextAlignment(TextAlignment.Center)
.HorizontalTextAlignment(TextAlignment.End)
.Margin(new Thickness(0,0,10,0)),
new Button("\x21E6").Command(vm.DeleteCharCommand).Column(2),
// using LINQ inside
labels.Select((label, i) =>
new Button(label)
.Row(i/3+1).Column(i%3)
.Command(vm.AddCharCommand).CommandParameter(label))
};
}
}
You can compare this to the XAML code:
Equivalent of code in XAML
from:
https://learn.microsoft.com/en-us/dotnet/maui/xaml/fundamentals/mvvm?view=net-maui-7.0
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.KeypadPage"
Title="Keypad Page">
<ContentPage.BindingContext>
<local:KeypadViewModel />
</ContentPage.BindingContext>
<Grid HorizontalOptions="Center" VerticalOptions="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80" />
<ColumnDefinition Width="80" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<Label Text="{Binding DisplayText}"
Margin="0,0,10,0" FontSize="20" LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center" HorizontalTextAlignment="End"
Grid.ColumnSpan="2" />
<Button Text="⇦" Command="{Binding DeleteCharCommand}" Grid.Column="2"/>
<Button Text="1" Command="{Binding AddCharCommand}" CommandParameter="1" Grid.Row="1" />
<Button Text="2" Command="{Binding AddCharCommand}" CommandParameter="2" Grid.Row="1" Grid.Column="1" />
<Button Text="3" Command="{Binding AddCharCommand}" CommandParameter="3" Grid.Row="1" Grid.Column="2" />
<Button Text="4" Command="{Binding AddCharCommand}" CommandParameter="4" Grid.Row="2" />
<Button Text="5" Command="{Binding AddCharCommand}" CommandParameter="5" Grid.Row="2" Grid.Column="1" />
<Button Text="6" Command="{Binding AddCharCommand}" CommandParameter="6" Grid.Row="2" Grid.Column="2" />
<Button Text="7" Command="{Binding AddCharCommand}" CommandParameter="7" Grid.Row="3" />
<Button Text="8" Command="{Binding AddCharCommand}" CommandParameter="8" Grid.Row="3" Grid.Column="1" />
<Button Text="9" Command="{Binding AddCharCommand}" CommandParameter="9" Grid.Row="3" Grid.Column="2" />
<Button Text="*" Command="{Binding AddCharCommand}" CommandParameter="*" Grid.Row="4" />
<Button Text="0" Command="{Binding AddCharCommand}" CommandParameter="0" Grid.Row="4" Grid.Column="1" />
<Button Text="#" Command="{Binding AddCharCommand}" CommandParameter="#" Grid.Row="4" Grid.Column="2" />
</Grid>
</ContentPage>
@mattleibow
I've created a project, that shows what an interface declaration would look like in code. Classes such as Grid or VerticalStackLayout already implement IEnumerable, so I could use braces for them. The Border class, on the other hand, does not implement IEnumerable now and I could not use it for it.
I used my library for this but only imported the package with extension methods. It covers all properties of MAUI controls.
You can load an example:
git clone https://github.com/idexus/HotReloadTest.git -b onlyextensions
Please reply if there is any progress on this PR.
Example Code
I'll try to look at this later this week.
@hartez
I'll try to look at this later this week.
I'm looking forward to it :)
You can also use the same extension methods to build styles in a type-safe way (it's from my example)
I've taken a look; letting this percolate a bit and I'll write up my thoughts on Monday.
@hartez you can also see my project page to better understand my approach
If I'm understanding this PR correctly, the whole point is to abuse the collection initializer syntax to avoid having to specify a property in the object initializer syntax. In order to do that, the PR marks several types which are not collections or things which could be considered enumerable as IEnumerable. It then implements the interface by returning a single arbitrary property as if it were an item in a collection (or, in some cases, by enumerating an arbitrary collection in the class).
And then it creates a set of extension methods named Add() which do not actually "add" an item (the generally understood purpose of an Add() method), but instead set a property.
So it's violating the defintion and spirit of IEnumerable and Add(), and it's doing so to shoehorn in an unconventional and potentially confusing use of a language feature in order to save some typing and make the code line up a particular way in the IDE.
This is a "no" right from the top. But to address some of the points brought up in the PR discussion:
This already works for things that are actual collections (e.g., the layouts), because that's the intent of the collection initializer syntax.
For things that aren't collections, like Label, it changes
new Label { Text = "text" };
to
new Label { "text" };
There's a comment about this being helpful for long text, but AFAICT the interface/extension method have no effect on that.
Removing this single property name doesn't strike me as a compelling reason to abuse the collection initializer syntax.
The stuff you mention in this comment is nifty, but you can already do that with layouts. Is there some compelling version of that example which requires Label or Button be IEnumerable?
There are some other comments and examples, but they don't seem to hinge on this PR. For example, in one comment you demonstrate procedurally generating a set of buttons. But this PR isn't required for that; I can already procedurally generate a UI in C# without any of the changes you're proposing. You point out that this isn't possible in XAML, but that's a given; XAML is a declarative UI language, so of course it can't do that.
You compare your code examples to XAML, or showing things that XAML can't do. But that's not the compelling case you have to make here. We can already build a UI without XAML; you would need to show why hacking collection initializers gives us a more compelling API in C#, which can already do all these things.
To be perfectly clear: I'm not advocating against folks building fluent extension APIs. I am specifically advocating against abusing language features for what amounts to a formatting change. That kind of feature abuse needs to come with a very compelling reason if I'm going to have to spend the next few years explaining to developers why Label.Add("text") replaces the the text of a Label rather than appending it.
@hartez first of all, thank you for your comprehensive answer
If I'm understanding this PR correctly, the whole point is to abuse the collection initializer syntax to avoid having to specify a property in the object initializer syntax. In order to do that, the PR marks several types which are not collections or things which could be considered enumerable as
IEnumerable. It then implements the interface by returning a single arbitrary property as if it were an item in a collection (or, in some cases, by enumerating an arbitrary collection in the class).
Generally speaking, it's about adding the IEnermerable interface for classes that have the [ContentProperty] attribute, and thus are containers, and logically speaking, you can put something in them, and in my understanding to give the possibility to use the Add method then. From a logical point of view, it can be a container that can hold many things, but there can also be a container that can hold one thing, in short, you can "add" one thing to it, and that was the original meaning of this PR.
And then it creates a set of extension methods named
Add()which do not actually "add" an item (the generally understood purpose of anAdd()method), but instead set a property.
This PR is not intended to add Add methods to the library, it is only to give the possibility of treating classes as containers for "things", and the sense of use would depend on the application creator or dependent library, but it gives such a possibility.
So it's violating the defintion and spirit of
IEnumerableandAdd(), and it's doing so to shoehorn in an unconventional and potentially confusing use of a language feature in order to save some typing and make the code line up a particular way in the IDE.This is a "no" right from the top. But to address some of the points brought up in the PR discussion:
I don't quite agree, as I said, since the classes already have the [ContentProperty] attribute in your library they are defined to be containers right now. Let's take "ScrollView" for example, yes, you can insert one view into it. But also asking how many objects are in it, you can answer "1" and return it if necessary. Of course, I could give an example here that in XAML this approach was actually used by you, hence the [ContentProperty] attribute. So I don't understand the resistance why not translate this directly into C# syntax.
This already works for things that are actual collections (e.g., the layouts), because that's the intent of the collection initializer syntax.
For things that aren't collections, like Label, it changes
new Label { Text = "text" };to
new Label { "text" };There's a comment about this being helpful for long text, but AFAICT the interface/extension method have no effect on that.
Removing this single property name doesn't strike me as a compelling reason to abuse the collection initializer syntax.
And in this case, I can agree. For views such as Label, I don't see the need to implement the IEnumerable interface. I only did this to be consistent with your approach, as it has a [ContentProperty] attribute defined in your library. So my argument followed what is already implemented.
The stuff you mention in this comment is nifty, but you can already do that with layouts. Is there some compelling version of that example which requires Label or Button be
IEnumerable?
This PR does not include adding an IEnumerable to a view like a Button. Its task is to add it only to multi-item and one-item containers. And the Label case I described above.
There are some other comments and examples, but they don't seem to hinge on this PR. For example, in one comment you demonstrate procedurally generating a set of buttons. But this PR isn't required for that; I can already procedurally generate a UI in C# without any of the changes you're proposing. You point out that this isn't possible in XAML, but that's a given; XAML is a declarative UI language, so of course it can't do that.
Yes, I have provided this example, and in fact I can already add a proper extension method for a type that implements IEnumerable, or just do it directly in the code somewhere else. My point here is only to show the consistency of my approach, and to enable the creation of the interface in a declarative way in the code, so that it reflects the structure of what will be displayed as closely as possible, which is an advantage in the case of XAML.
You compare your code examples to XAML, or showing things that XAML can't do. But that's not the compelling case you have to make here. We can already build a UI without XAML; you would need to show why hacking collection initializers gives us a more compelling API in C#, which can already do all these things.
Yes, I agree many of the things I'm talking about can already be done directly in the code, but what I'm talking about is creating a method that is consistent in its intention, and gives you the ability to create an interface in an intuitive way, giving it similarity to XAML, but also power of C#, while not departing from the MVVM model. This is what I am trying to get in my library.
To be perfectly clear: I'm not advocating against folks building fluent extension APIs. I am specifically advocating against abusing language features for what amounts to a formatting change. That kind of feature abuse needs to come with a very compelling reason if I'm going to have to spend the next few years explaining to developers why
Label.Add("text")replaces the the text of a Label rather than appending it.
I've already explained the topic of Label, so I won't repeat myself :) As for overusing language syntax, sometimes you have to think differently to get ahead. And I wouldn't call it abuse, but using the syntax to achieve a goal :)
I will not compare this solution to XAML here, and I will not give such examples, because I see that this is a sensitive point. However, I will show a comparison between the two approaches in creating an interface directly in the C# code.
The following two examples create the same view. In the first, I assume, apart from adding IEnumerable, the existence of additional constructors, which were mentioned in the discussion.
Please judge for yourself.
Content = new ScrollView(e => e.BackgroundColor(Colors.Black))
{
new VerticalStackLayout(out var vStack, e => e.VerticalOptions(LayoutOptions.Center))
{
new Label(out var label)
.TextColor(e => e.DynamicResource("myColor"))
.Text("Only in Code :)")
.FontSize(45),
new Slider(out var slider)
.Minimum(1).Maximum(30)
.WidthRequest(400)
.Value(e => e.Path("SliderValue"))
.Margin(50, 30)
.OnValueChanged(slider => button.IsEnabled = slider.Value < 10),
new Border(e => e
.SizeRequest(270, 450)
.BackgroundColor(AppColors.Gray950)
.StrokeShape(new RoundRectangle().CornerRadius(40)))
{
new Grid(e => e.RowDefinitions(e => e.Star(1.3).Star(3).Star().Star()))
{
new Label()
.Text(e => e.Path("Value").Source(slider).StringFormat("Value : {0:F1}"))
.FontSize(40),
new Image().Source("dotnet_bot.png").Row(1),
new Label()
.Text("Hello, World!")
.Row(2)
.FontSize(30)
.TextColor(Colors.DarkGray),
new Switch(out testSwitch).Row(3)
.CenterInContainer()
},
},
new Button(out button)
.Text("Click me")
.Margin(30)
.OnClicked(async (Button b) =>
{
count++;
b.Text = $"Clicked {count} ";
b.Text += count == 1 ? "time" : "times";
await vStack.RotateYTo(((count % 4) switch { 0 => 0, 1 => 20, 2 => 0, _ => -20 }));
await label.RotateTo(360 * (count % 2), 300);
})
}
};
VS:
// inside constructor
var scrollView = new ScrollView();
scrollView.BackgroundColor = Colors.Black;
var vStack = new VerticalStackLayout();
vStack.VerticalOptions = LayoutOptions.Center;
var label = new Label
{
Text = "Only in Code :)",
FontSize = 45
};
label.SetDynamicResource(Label.TextColorProperty, "myColor");
var slider = new Slider
{
Minimum = 1,
Maximum = 30,
WidthRequest = 400,
Margin = new Thickness(50, 30)
};
slider.SetBinding(Slider.ValueProperty, "SliderValue");
slider.ValueChanged += (sender, e) =>
{
button.IsEnabled = e.NewValue < 10;
};
var grid = new Grid
{
RowDefinitions = new RowDefinitionCollection()
{
new RowDefinition() { Height = new GridLength(1.3, GridUnitType.Star) },
new RowDefinition() { Height = new GridLength(3, GridUnitType.Star) },
new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) },
new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) }
}
};
var valueLabel = new Label();
valueLabel.FontSize = 40;
valueLabel.SetBinding(Label.TextProperty, new Binding("Value", source: slider, stringFormat: "Value : {0:F1}"));
grid.Children.Add(valueLabel);
var image = new Image();
image.Source = "dotnet_bot.png";
Grid.SetRow(image, 1);
grid.Children.Add(image);
var helloLabel = new Label
{
Text = "Hello, World!",
FontSize = 30,
TextColor = Colors.DarkGray
};
Grid.SetRow(helloLabel, 2);
grid.Children.Add(helloLabel);
var testSwitch = new Switch();
testSwitch.HorizontalOptions(LayoutOptions.Center);
Grid.SetRow(testSwitch, 3);
Grid.SetColumn(testSwitch, 0);
Grid.SetColumnSpan(testSwitch, 2);
grid.Children.Add(testSwitch);
var border = new Border
{
WidthRequest = 270,
HeightRequest = 450,
StrokeShape = new RoundRectangle { CornerRadius = 40 },
BackgroundColor = AppColors.Gray950,
Content = grid
};
button = new Button
{
Text = "Click me",
Margin = 30
};
button.Clicked += async (sender, e) =>
{
count++;
button.Text = $"Clicked {count} ";
button.Text += count == 1 ? "time" : "times";
await vStack.RotateYTo(((count % 4) switch { 0 => 0, 1 => 20, 2 => 0, _ => -20 }));
await label.RotateTo(360 * (count % 2), 300);
};
vStack.Children.Add(label);
vStack.Children.Add(slider);
vStack.Children.Add(border);
vStack.Children.Add(button);
scrollView.Content = vStack;
Content = scrollView;
I decided to give AI some examples and teach it the rules, and check how its code generation is going according to these rules.
here is the result
Generally speaking, it's about adding the IEnermerable interface for classes that have the [ContentProperty] attribute, and thus are containers, and logically speaking, you can put something in them, and in my understanding to give the possibility to use the Add method then. From a logical point of view, it can be a container that can hold many things, but there can also be a container that can hold one thing, in short, you can "add" one thing to it, and that was the original meaning of this PR.
IEnumerable and ContentProperty are two different things with two different purposes.
ContentProperty is just a mapping from XML content to a single property on an object; the "content" is the content of the XML tag. Label is not a "container" for text; Label has a Text property, and the ContentProperty attribute tells the XAML parser to map the XML content of the <Label/> tag to the Label.Text property.
IEnumerable just means that a thing can list a set of items; it does not imply that it's a container. And generally speaking, it's meant for something which can list "zero to many things", not "exactly one thing". Most importantly here, though, is that it doesn't allow you to "set content". IEnumerable has nothing to do with setting content unless you are trying to support collection initializers, and then only if you also create an Add() method.
So without the extension methods you provide in the sample section, this PR doesn't do anything except add IEnumerable to some classes which are not collections and don't otherwise have any use for the IEnumerable interface. At best, it's giving other folks the option of adding confusing Add() extension methods which do not add things to collections.
And in this case, I can agree. For views such as Label, I don't see the need to implement the IEnumerable interface. I only did this to be consistent with your approach, as it has a [ContentProperty] attribute defined in your library. So my argument followed what is already implemented.
Since ContentProperty and IEnumerable are not the same thing, then, we can remove IEnumerable from all of the places where this PR adds it. What, then, is left?
Yes, I agree many of the things I'm talking about can already be done directly in the code, but what I'm talking about is creating a method that is consistent in its intention, and gives you the ability to create an interface in an intuitive way, giving it similarity to XAML, but also power of C#, while not departing from the MVVM model. This is what I am trying to get in my library.
I've already explained the topic of Label, so I won't repeat myself :) As for overusing language syntax, sometimes you have to think differently to get ahead. And I wouldn't call it abuse, but using the syntax to achieve a goal :)
So the "goal" you're trying to achieve is to have a declarative API in C# that mirrors what's already available in XAML, and allows some procedural C# as well? That's fine; you're more than free to do that in your own library. And as far as I can tell, you've been able to do without this PR, except for having to do
Content = new ScrollView()
{
Content = new VerticalStackLayout()
{
instead of
Content = new ScrollView()
{
new VerticalStackLayout()
Aside from abusing collection initializers to initialize things which are not collections, what does this PR allow that cannot already be done? If the only thing is gives anyone is the possibility of abusing collection initializers, then I can't see any reason for it to be merged.
As a thought exercise, let's say Label : IEnumerable, and that we add your extension method:
public static T Add<T>(this T element, string text)
where T : Label
{
element.Text = text;
return element;
}
This would now be legal syntax:
var label = new Label { "Some text", "Some more text", "Yet more text" };
After which label.Text is "Yet more text".
Or Border : IEnumerable, with
public static T Add<T>(this T element, View content)
where T : Border
{
element.Content = content;
return element;
}
Which makes this legal:
var border = new Border { new Button { Text = "Button 1" }, new Button { Text = "Button 2" }, new Button { Text = "Button 3" } };
But after that runs, border.Content is just a single button.
This is what I mean by abuse of collection initializers. The language feature has a specific purpose - to make the syntax for creating collections less verbose. With this stuff in place, we'd be violating the expectations of users about what happens when you use this language feature. We'd also be wildly inconsistent with everything else in the C# ecosystem.
I think we see the problem differently.
I perfectly understand the meaning of the [ContentProperty] attribute. I'll take a thought exercise too. Suppose we have a bicycle, At first no one sits on it. Is the number of cyclists that can ride this bike enumerable? Yes, it is and is one. Can two people ride this bike, no. There comes a time when you have to tell the other person that he won't come in.
public static T Add<T>(this T bike, Rider rider)
where T : Bike
{
if (bike.Rider != null) throw new ArgumentException("Sorry, this bike is taken");
bike.Rider = rider;
return bike;
}
In addition, what we put into the container using the Add method is always enumerable. Whether it's a function, View, or some other object. And here is the big difference between IEnumerable<T> and IEnumerable because the first interface tells us what we can put in the bag and the second one not if you think outside the box ;)
This is what I mean by abuse of collection initializers. The language feature has a specific purpose - to make the syntax for creating collections less verbose. With this stuff in place, we'd be violating the expectations of users about what happens when you use this language feature. We'd also be wildly inconsistent with everything else in the C# ecosystem.
I will say one more thing, it all depends on the level of abstraction at which we want to stay, and I'm not just talking about the curly braces or the Add method, I'm talking about the whole spectrum of possibilities that can be achieved even with the current language syntax.