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

[MVUX] Custom Control Style not set properly when bound to the model #2400

Closed
MartinZikmund opened this issue Jun 27, 2024 Discussed in #2372 · 8 comments
Closed

[MVUX] Custom Control Style not set properly when bound to the model #2400

MartinZikmund opened this issue Jun 27, 2024 Discussed in #2372 · 8 comments
Assignees

Comments

@MartinZikmund
Copy link
Member

Discussed in #2372

Originally posted by kucint June 20, 2024
Let´s create a simple Control:

internal class MyControl : Control
{
    public MyControl()
    {
        DefaultStyleKey = typeof(MyControl);
        Style = StyleFactory.CreateDefaultStyle();
    }
}

Style for this control is created dynamically as follows:

internal static class StyleFactory
{
    // Here we define the default UI of the control, it can also be change by user.
    public static Style<MyControl> CreateDefaultStyle() => new Style<MyControl>()
        .Setters(s => s
            .BorderBrush(Colors.Blue)
            .BorderThickness(1)
            .AutoUpdate(true)

            .Template(ctrl => new StackPanel()
                .BorderBrush(x => x.TemplateBind(() => ctrl.BorderBrush))
                .BorderThickness(x => x.TemplateBind(() => ctrl.BorderThickness))
                .Children(
                    new TextBlock().Text("default style"),
                    new TextBlock().Text(x => x.TemplateBind(() => ctrl.AutoUpdate))
                )
            )
        );

    public static Style<MyControl> CreateStyle(string txt, Color color) => new Style<MyControl>()
        .Setters(s => s
            .BorderBrush(color)
            .BorderThickness(3)
            .AutoUpdate(true)

            .Template(ctrl => new StackPanel()
                .BorderBrush(x => x.TemplateBind(() => ctrl.BorderBrush))
                .BorderThickness(x => x.TemplateBind(() => ctrl.BorderThickness))
                .Children(
                    new TextBlock().Text(txt),
                    new TextBlock().Text(x => x.TemplateBind(() => ctrl.AutoUpdate))
                )
            )
        );
}

Lest consume now the control and apply different styles to it:

public sealed partial class MainPage : Page
{
    public MainPage() => this
        .DataContext(new BindableMainPageModel(), (page, vm) => page
        .Background(ThemeResource.Get<Brush>("ApplicationPageBackgroundThemeBrush"))
        .Content(
            new StackPanel()
            .VerticalAlignment(VerticalAlignment.Center)
            .HorizontalAlignment(HorizontalAlignment.Center)
            .Children(
                new TextBlock()
                .Text("expected style: default style -> passed"),
                new MyControl()
                .Margin(0, 0, 0, 10),

                new TextBlock()
                .Text("expected style: style instantiated in the control -> passed"),
                new MyControl()
                .Style(StyleFactory.CreateStyle("style instantiated in the control", Colors.DarkOrchid))
                .Margin(0, 0, 0, 10),

                new TextBlock()
                .Text("expected style: style instantiated in the Frame -> passed"),
                new MyControl()
                .Margin(0, 0, 0, 10)
                .Style(new Style<MyControl>()
                    .Setters(s => s
                    .BorderBrush(Colors.DarkGreen)
                    .BorderThickness(5)
                    .AutoUpdate(true)
                    .Template(ctrl => new StackPanel()
                        .BorderBrush(x => x.TemplateBind(() => ctrl.BorderBrush))
                        .BorderThickness(x => x.TemplateBind(() => ctrl.BorderThickness))
                        .Children(
                            new TextBlock().Text("style instantiated in the Frame"),
                            new TextBlock().Text(x => x.TemplateBind(() => ctrl.AutoUpdate))
                        )
                    ))),

                new TextBlock()
                .Text("expected style: style instantiated in the model -> failed"),
                new MyControl()
                .Margin(0, 0, 0, 30)
                .Style(() => vm.Style),

                new TextBlock()
                .Text("expected style: style instantiated in the model (A, B, C) -> failed"),
                new ItemsControl()
                .ItemsSource(() => vm.Items)
                .ItemTemplate<Item>(item => new MyControl()
                    .Style(x => x.Binding(() => item.Style)))
                )
            )
        );
}

Model:

internal partial record MainPageModel
{
    public IState<Style> Style => State<Style>.Value(this, () => StyleFactory.CreateStyle("Style from model", Colors.HotPink));

    public IListState<Item> Items => ListState<Item>.Value(this, () =>
    [
        new Item("Item A", StyleFactory.CreateStyle("Style A from model", Colors.Red)),
        new Item("Item B", StyleFactory.CreateStyle("Style B from model", Colors.Green)),
        new Item("Item C", StyleFactory.CreateStyle("Style C from model", Colors.Blue)),
    ]);
}

internal record Item(string Name, Style Style);

In most of the cases, dynamically generated Style is applied as expected, but not when it comes from bound MVU model:

image

Tested on Windows: net8.0-desktop;

Note that when run on Windows with net8.0-windows10.0.19041; control does not render at all.
But this is another issue....
image

Uno version: "Uno.Sdk": "5.2.161"

Repro: UnoStyleAsPropertyApp.zip

Note that in the example above all styles are instantiated in the control so the static styles are nor created at all.
This is due to avoid interference with another issue #2349

@Youssef1313 Youssef1313 self-assigned this Jul 9, 2024
@dr1rrb
Copy link
Member

dr1rrb commented Jul 10, 2024

Hummm, for me the issue is here:

public IListState<Item> Items => ListState<Item>.Value(this, () =>
    [
        new Item("Item A", StyleFactory.CreateStyle("Style A from model", Colors.Red)),
        new Item("Item B", StyleFactory.CreateStyle("Style B from model", Colors.Green)),
        new Item("Item C", StyleFactory.CreateStyle("Style C from model", Colors.Blue)),
    ]);

The delegate is expected to be invoked from a background thread, that's why the style does not work at all on windows, and might have some weird behaviors with uno (where we are not validating the UI thread as strongly as windows). The StyleFactory should be async and ensure to go back on the right UI thread ...

@Youssef1313
Copy link
Member

Youssef1313 commented Jul 11, 2024

Generally, DependencyObject should only be accessed from UI thread.

However, somehow, this is now working (given the latest bug fixes on C# Markup) on both WinAppSDK and Uno. But I agree with David that this feels wrong and you probably shouldn't rely on things running that way. For Uno specifically, accessing the DP system on background thread may appear like it's working, but can then show random race conditions that are hard to track down. Such race conditions in Uno can end up storing a DP value in place of another, then when the other one is read you can end up with incorrect value or even InvalidCastException, etc.

@kucint
Copy link

kucint commented Jul 11, 2024

thanks @dr1rrb @Youssef1313
well, ........ I do not understand......
this is just typical MVU code....
you mean, I am not supposed to create Style object in Update delegate because Style is an DependencyObject?
and how StyleFactory can be async when it does not call any async methods?

Q1: could someone help and provide the corrected code for StyleFactory?
Q2: how am I suppose to change dynamically a style dependent of consumer choice (MVU state)?

@Youssef1313
Copy link
Member

@kucint What I'm seeing currently is that the code is actually run on the UI thread, so it will be currently working with latest C# Markup dev releases (using latest Uno.Sdk dev).

I'll leave it to @dr1rrb to comment on the MVUX part on whether it was expected that the delegate is run on the UI thread or not, and how to properly write the StyleFactory

@Youssef1313 Youssef1313 reopened this Jul 11, 2024
@Youssef1313 Youssef1313 assigned dr1rrb and unassigned Youssef1313 Jul 11, 2024
@dr1rrb
Copy link
Member

dr1rrb commented Jul 19, 2024

Hey @kucint this is only about creating the style on the dispatcher. You ave 2 options:

1. Embed the dispatcher in the StyleFactory

For this you have to convert your StyleFactory into a non static class and do something like this:

internal class StyleFactory(DispatcherQueue Dispatcher)
{
     /* .../.. */

    public ValueTask<Style<MyControl>> CreateStyle(string txt, Color color) => Dispatcher.ExecuteAsync(async () => new Style<MyControl>()
        .Setters(s => /* .../.. */);
}

And then your model

internal partial record MainPageModel(StyleFactory Styles)
{
    public IState<Style> Style => State<Style>.Async(this, async _ => await Styles.CreateStyle("Style from model", Colors.HotPink));

    public IListState<Item> Items => ListState<Item>.Async(this, async _ =>
    [
        new Item("Item A", await Styles.CreateStyle("Style A from model", Colors.Red)),
        new Item("Item B", await Styles.CreateStyle("Style B from model", Colors.Green)),
        new Item("Item C", await Styles.CreateStyle("Style C from model", Colors.Blue))
    ]);
}

2. Defer to the caller the responsibility to invoke the factory on the right thread

In your case this means to inject the dispatcher in the MainPageModel:

internal partial record MainPageModel(DispatcherQueue Dispatcher)
{
    public IState<Style> Style => State<Style>.Value(this, () => StyleFactory.CreateStyle("Style from model", Colors.HotPink));

    public IListState<Item> Items => ListState<Item>.Async(this, async _ => await Dispatcher.ExecuteAsync(async () => ImmutableList.Create(
        new Item("Item A", StyleFactory.CreateStyle("Style A from model", Colors.Red)),
        new Item("Item B", StyleFactory.CreateStyle("Style B from model", Colors.Green)),
        new Item("Item C", StyleFactory.CreateStyle("Style C from model", Colors.Blue))
    )));
}

@kucint
Copy link

kucint commented Jul 19, 2024

@dr1rrb, I see what you mean. thanks!

@MartinZikmund
Copy link
Member Author

@kucint can this be closed?

@kucint
Copy link

kucint commented Jul 24, 2024

@kucint can this be closed?

I did not try the proposed solution, but ... I guess, yes.
if I will have further questions, I will ask to reopen it. OK?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants