-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Simplification of user interface declaration in code #12728
Conversation
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. |
public partial class Label : View, IFontElement, ITextElement, ITextAlignmentElement, ILineHeightElement, IElementConfiguration<Label>, IDecorableTextElement, IPaddingElement | ||
public partial class Label : View, IFontElement, ITextElement, ITextAlignmentElement, ILineHeightElement, IElementConfiguration<Label>, IDecorableTextElement, IPaddingElement, IEnumerable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure Label (and similar) are useful? Maybe these "primitive content" ones don't need this? And maybe Span too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can be useful when you are writing very long text
new Label
{
$"Lorem Ipsum is simply dummy text of the printing and typesetting " +
$"industry. Lorem Ipsum has been the industry's standard dummy text ever " +
$"since the 1500s, when an unknown printer took a galley of type and scrambled " +
$"it to make a type specimen book. It has survived not only five centuries, " +
$"but also the leap into electronic typesetting, remaining essentially " +
$"unchanged. It was popularised in the 1960s with the release of Letraset " +
$"sheets containing Lorem Ipsum passages, and more recently with desktop " +
$"publishing software like Aldus PageMaker including versions of Lorem Ipsum."
},
/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? |
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? |
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 ParametersOverloaded Constructors with Default ParametersAdd a constructor accepting every Property as a default/optional parameter. ImplementationSince every control is already a
Source Generator Outputnamespace 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 UsageContent = 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 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. |
Thanks for involving me - I'm glad to see MAUI improvements for developers using C# for markup! My 2cts on above discussion:
|
@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 useUsing You can implement extension 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
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 XAMLfrom: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> |
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:
Please reply if there is any progress on this PR. |
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 And then it creates a set of extension methods named So it's violating the defintion and spirit of 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
to
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 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 |
@hartez first of all, thank you for your comprehensive answer
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
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.
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.
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.
This PR does not include adding an
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.
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 :) |
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 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; |
So without the extension methods you provide in the sample section, this PR doesn't do anything except add
Since
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
instead of
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
This would now be legal syntax:
After which Or
Which makes this legal:
But after that runs, 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 |
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. |
We don't need all this extra effort to enforce the idea of a "object which has exactly one of something". The language already has that, and it's called a "property". If I want to ensure that a bike has exactly 1 rider, I just need a property
But that would confuse everyone who understands what
because it compiles, and they would only find out at runtime that it doesn't work that way. If we really want to promote one property to be "special" for the purposes of initialization, we can do that with a constructor overload:
There, now it's Thinking outside the box is not a virtue unto itself; the ideas we find outside that box have to add more value than they remove. And this is programming; we value clarity and consistency. |
I can't help it you can't see what it brings. I'm sorry to say, but I see a big inconsistency in your approach and reasoning. The XAML below also compiles, in fact it doesn't even throw an exception and sorry, I don't see any clarity or consistency here. <?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="testApp.MainPage">
<Border>
<Label Text="Rider 1"></Label>
<Label Text="Rider 2"></Label>
<Label Text="Rider 3"></Label>
</Border>
</ContentPage> I will leave it without further comment. |
Description of Change
It adds the ability to declare content inside curly braces.
This PR:
IEnumerable
interface for classes that do not already have theIList
interface implemented and have the[ContentProperty]
attribute.FluentExamplePage.cs
with sample extension methods (Maui.Controls.Sample/Pages/Fluent) and places it in the "Others" section of the sample applicationContentPropertyUnitTest.cs
file to test the content property attribute and implementation of theIEnumerable
interfaceUsage example:
sample extension methods are declared in the Maui.Controls.Sample application folder /Pages/Fluent/Extensions
Issues Fixed
Fixes #12678