VincentH-Net / CSharpForMarkup

Concise, declarative C# UI markup for .NET browser / native UI frameworks
MIT License
748 stars 43 forks source link

ContentDialog is not parsed #19

Closed YegorStepanov closed 2 years ago

YegorStepanov commented 2 years ago
screenshot ![image](https://user-images.githubusercontent.com/19392641/146653393-fbccf459-d288-4e72-8514-1871032d993f.png)

Steps to reproduce:

Add to example

FlutterPage.cs:

Button("Show ContentDialog").Command(A),

ContentDialog(
    StackPanel(TextBox(Text: "TextBox"))
)
.PrimaryButtonText("PrimaryText")
.Title("Title")
Assign(out contentDialog),

FlutterPage.logic.cs:

public FlutterPage()
{
    ...
    contentDialog.XamlRoot = App.Current.XamlRoot();
}

ContentDialog contentDialog;
ICommand aCommand;
public ICommand A => aCommand ??= new RelayCommandAsync(async () => await contentDialog.ShowAsync());

App.xaml.cs:

internal XamlRoot XamlRoot() => _window.Content.XamlRoot;

Another questions:

Is it possible to set object content through method call?

Something like:

ContentDialog(PrimaryButtonText: "abc", Title: "Title").Content(StackPanel(..))

instead of

ContentDialog(StackPanel(..)).PrimaryButtonText("abc").Title("Title")

What's about performance?

I should add to Treeview 10K elements sometimes. Is the performance impact near zero (in comparison with XAML)? What performance scenarios do we need to be careful with?

Thank you for freeing us from XAML❤️

VincentH-Net commented 2 years ago

This is because ContentDialog.Content takes an object; the repro code passes in the markup object, which is displayed as it's type name: image

You can use .UI to pass in the UI object contained in the markup object:

ContentDialog(
    StackPanel(TextBox(Text: "TextBox")).UI
)

and then the ContentDialog displays as intended: image

You only have to explicitly pass in .UI where an object is expected; all markup objects have an implicit operator to automatically return .UI where a UIElement (or derived class) is expected.

VincentH-Net commented 2 years ago

Is it possible to set object content through method call?

No, that would not be safe because markup objects are performance optimized to have no more than one instance per type.

This means that there is no GC pressure from markup object instances - the only object instances created are the real UI objects. The single markup object instance is what is used to form the fluent API chain.

However that requires that child controls are not created in the fluent API chain that sets properties on the parent control. In the case that the child control is of the same type as the parent control, that would break the chain.

This is why the C# Markup API is shaped so that child controls are passed in as parameters to the start of a parent control chain. That ensures that the child control chains are fully evaluated before the parent control chain starts.

Also see this explanation in the source

VincentH-Net commented 2 years ago

What's about performance? I should add to Treeview 10K elements sometimes. Is the performance impact near zero (in comparison with XAML)? What performance scenarios do we need to be careful with?

I have not measured this yet, but C# Markup should indeed be as fast or faster than XAML.

C# Markup creates no objects in addition to the UI object itself and every property is a single assignment without the need for a lookup or parse.

On Windows, the XAML engine is written in C++ which is faster than C#, but it has to do a lot more work to parse XAML and create a UI object and set all it's properties.

In Uno Platform, the XAML is converted to C# at build time via code generation; this generated code is not smaller than the C# Markup code. In the case of the webassembly target of Uno Platform, C# Markup could be slower than the C# code that Uno Platform generates from XAML - I recall that Uno Platform optimized that code to avoid C# generics, which at some point in time were slower in webassembly. However that may have changed - C# webassembly is constantly being optimized. A webassembly performance test is on my wish list.

VincentH-Net commented 2 years ago

Thank you for freeing us from XAML❤️

You are welcome! I love this type of feedback - spread the word!

YegorStepanov commented 2 years ago

@VincentH-Net You have done a wonderful job instead of MS!

ContentDialog(Title: "Title", 
    Content: 
    StackPanel().UI
VincentH-Net commented 2 years ago

What about adding optional Content parameter?

One of the nice things about C# Markup when compared to XAML is how easy it is to add your own convenience API's. What you describe above you can add as a 2-liner to your own extension methods class, e.g.: MarkupExtensions.cs:

public static ContentDialog ContentDialog(string Title, DependencyObject Content)
    => Helpers.ContentDialog(content: Content?.UI).Title(Title);

I expect to add helpers like this for common cases where .UI is needed. If you encounter more cases, best would be to create an issue

It turns out I can never use the property's ctor with children, right? i.e. the constructors for collections

The generated factory method for views that can have children only takes one params collection, but nothing prevents you from adding overloads with more parameters before that e.g.:

public static StackPanel StackPanel(UI.Thickness Padding = default, params UI.UIElement[] Children)
    => Helpers.StackPanel(Children).Padding(Padding);
...           
StackPanel(Padding: new UI.Thickness(), TextBox(Text: "TextBox"), Button("Click Me"))

However, you have to be careful not to introduce ambiguities and parameters only work well for properties that take primitive types. The extension method way to set properties allows to add nice convenience overloads, e.g.:

StackPanel ()
    .Padding (0)
    .Padding (2, 4)
    .Paddings (top: 6)
    .Padding().Bind (...)

After some experimentation, I found that having the children of a layout first and the properties at the end nicely moves the less important details out of the way. In my experience it makes the whole markup more quickly scannable

YegorStepanov commented 2 years ago

@VincentH-Net

Adding ContentDialog(DependencyObject Content) hides the default Helpers.ContentDialog(), so I need to create HelpersOfHelpers :) class and call it without using static.

I don't know how to name the issue, so I ask it here.

What's about custom controls?

If the default controls generate by SourceGenerators, I suggest adding [Markup(typeof(MyControl))] that generates Markup's methods automatically. It seems to be possible to implement it. There is a C#-only part of WinUI3 - WindowsCommunityToolkit, that as I understand, not supporting.

Is there no support for VisualStateManager?

image

Should I call Invoke for setting simple properties like ScrollViewer.HorizontalScrollMode?

🎄🎄🎄

VincentH-Net commented 2 years ago

@YegorStepanov wrote:

Adding ContentDialog(DependencyObject Content) hides the default Helpers.ContentDialog(), so I need to create HelpersOfHelpers :) class and call it without using static.

Defining your own additional helpers in the app works well, also with using static, as long as you take care with the overload method signatures. It helps to not give parameters default values but instead define separate overloads for different parameter combinations. This avoids ambiguities / hiding.

In your case, when you add these overloads in MarkupExtensions.cs in the example solution:

    public static ContentDialog ContentDialog(string Title, DependencyObject Content)
        => Helpers.ContentDialog(content: Content?.UI).Title(Title);

    public static ContentDialog ContentDialog(DependencyObject Content)
        => Helpers.ContentDialog(content: Content?.UI);

And you add global using static WinUICsMarkupExamples.MarkupExtensions; to GlobalUsings.cs (or a regular using static to the page),

All of these variations work - nothing is hidden:

ContentDialog(
    "Title 1",
    TextBlock("This is content 1")
)  .PrimaryButtonText("PrimaryText")

ContentDialog(
    TextBlock("This is content 2")
)  .Title ("Title 2")
   .PrimaryButtonText ("PrimaryText")

ContentDialog("This is content 3")
   .Title("Title 3")
   .PrimaryButtonText("PrimaryText")

ContentDialog()
   .Title("Title 4")
   .PrimaryButtonText("PrimaryText")

Let me know if this works for you. NJoy!

YegorStepanov commented 2 years ago

@VincentH-Net You are right, it's working.

I haven't created a new class and simply added these methods to UI-class.