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

x:Load attribute #6853

Open
workgroupengineering opened this issue Nov 3, 2021 · 9 comments
Open

x:Load attribute #6853

workgroupengineering opened this issue Nov 3, 2021 · 9 comments

Comments

@workgroupengineering
Copy link
Contributor

Is your feature request related to a problem? Please describe.
https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/x-load-attribute
Describe the solution you'd like
A clear and concise description of what you want to happen.

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

Additional context
Add any other context or screenshots about the feature request here.

@maxkatz6
Copy link
Member

maxkatz6 commented Nov 3, 2021

#1434

@kekekeks
Copy link
Member

kekekeks commented Nov 4, 2021

A basic implementation would be quite easy to do. We could have something like this:

    public class DeferLoad : Control
    {
        private IControl _control;
        
        public static readonly StyledProperty<bool> LoadProperty = AvaloniaProperty.Register<DeferLoad, bool>(
            "Load");

        public bool Load
        {
            get => GetValue(LoadProperty);
            set => SetValue(LoadProperty, value);
        }
        
        [Content, TemplateContent]
        public object DeferredContent { get; set; }

        public IControl Control => _control;

        static DeferLoad()
        {
            LoadProperty.Changed.AddClassHandler<DeferLoad>((c, e) =>
            {
                if (e.NewValue is bool v)
                    c.DoLoad(v);
            });
        }
        
        protected override Size MeasureOverride(Size availableSize) 
            => LayoutHelper.MeasureChild(_control, availableSize, default);

        protected override Size ArrangeOverride(Size finalSize) 
            => LayoutHelper.ArrangeChild(_control, finalSize, default);


        private void DoLoad(bool load)
        {
            if((_control != null) == load)
                return;

            if (load)
            {
                _control = TemplateContent.Load(DeferredContent).Control;
                ((ISetLogicalParent)_control).SetParent(this);
                VisualChildren.Add(_control);
                LogicalChildren.Add(_control);
            }
            else
            {
                ((ISetLogicalParent)_control).SetParent(null);
                LogicalChildren.Clear();
                VisualChildren.Remove(_control);
                _control = null;
            }
            InvalidateMeasure();
        }
    }

Usage:

        <CheckBox x:Name="Load">Load</CheckBox>
        <example:DeferLoad Load="{Binding #Load.IsChecked}"> 
            <Button>Hello I'm deferred content</Button>
        </example:DeferLoad>

(you can just copy that class to your app right now, BTW)

Then XAML compiler could rewrite:

<Button x:Load="{Binding LoadButton}" >Test</Button>

into

<DeferredContentContainer>
  <Button x:Load="{Binding LoadButton}" >Test</Button>
</DeferredContentContainer>

which is also kinda easy to do. But we need to actually design the feature first.
It would be really helpful to investigate how exactly this feature is implemented in UWP.
Is placeholder element somehow being replaced with a loaded one or just adds it as its own child?

How does it work with x:Name and fields in codebehind?

@robloo
Copy link
Contributor

robloo commented Nov 5, 2021

FYI: This is heavily used on the Uno Platform for mobile devices (and even desktop due to how slow it is). There was just no way to get acceptable performance otherwise. I really think it's going to be needed for Avalonia as well once mobile support comes online and is more widely used: #1602 (comment)

I still think #1434 titled 'Conditional XAML' is confusing as that is a different feature of Microsoft XAML. I would vote to keep this issue open tracking x:Load specifically.

@robloo
Copy link
Contributor

robloo commented Nov 5, 2021

@kekekeks

How does it work with x:Name and fields in codebehind?

x:Name and x:FieldModifier are transparent to developers in code-behind. These properties are just used by the code generator and 'Name' and the field modifier are set directly in the generated code. You can access controls by name automatically in code-behind without having to Find them first. Personally, I think of the x: namespace as meaning it is handled by code (x: is a separate XAML namespace for the XAML-defined language elements. Fundamentally, UWP doesn't handle these any differently than Avalonia.NameGenerator

For x:Load things are a little more complicated. It's simple enough to use in XAML. The dev just specifies x:Load="False" and the FrameworkElement will not be constructed and added to the visual tree automatically. Instead, in code-behind, the application must use code such as this.FindName(nameof(this.MyGrid)); otherwise, the reference to MyGrid is always null. (MyGrid will be an auto-generated field in the .g.cs file). The logic for constructing a control and adding it to the visual tree when x:Load is false is not handled by auto-generated files. This appears to be handled within FindName itself.

Edit: I realize I probably misunderstood your question. I imagine there is a placeholder used by WinUI after skimming through the docs. Setting the Name property would have to be deferred too somehow; although generated code references for named controls in the window/page would be the same. The Uno Platform implementation has answered these questions though so it might be useful digging through how they did things.

@kekekeks
Copy link
Member

kekekeks commented Nov 5, 2021

I was thinking about rewriting x:Load to DeferredContentContainer approach and it seems that it breaks several interactions with the rest of the framework:

  1. Where should attached properties go? Panels like Grid and DockPanel require them to be set on their direct child. Other properties must to be set on the control itself
  2. Selectors like Grid > Button will simply not work if Button was rewritten to <DeferredContentContainer><Button>..., the selector should look like Grid > DeferredContentContainer > Button

So while being a feasible API to provide as is, we can't use it as direct x:Load implementation.

However, from my understanding x:Load requires special support from container to properly hide the placeholder from the user code, i. e. it doesn't seem to be possible to assign x:Load-marked control to a custom property, e. g.

<local:CustomControl x:Name="Parent">
  <local:CustomControl.Control>
    <local:MyButton
      x:Name="HelloWorld"
      x:Load="True"
      Content="ASDASDASD" />
  </local:CustomControl.Control>
</local:CustomControl>

fails with

Windows.UI.Xaml.Markup.XamlParseException: 'The text associated with this error code could not be found.
Failed to assign to property 'App3.CustomControl.Control'. [Line: 22 Position: 11]'

(tested by @maxkatz6 )

I don't think we need that special support for property assignments since we could just set them in deferred way like we do with bindings.

For panels however that special support seems to be provided by UIElementCollection.

The problem is, we don't have separate Items and ItemsSource like WPF/UWP do, so we can do that on collection level.

The way I see it could be done is by creating an special shadow collection that's linked to the actual list and populated by compiler instead of said list. Shadow collection would track the load status of individual elements and push changes to the target list.

The only limitation of that approach I see is that target list has to be essentially owned by XAML and not modified by anything else, which, I suppose, is OK for most cases.

For cases where it's not appropriate, one could use DeferredContentContainer API that we'll provide separately.

@grokys
Copy link
Member

grokys commented Nov 5, 2021

Maybe we need to move to having a separate Items and ItemsSource property like WPF/UWP? It'd be a big breaking change, but would also simplify some other parts of the code as well, and would make porting easier.

@Whiletru3
Copy link
Contributor

A basic implementation would be quite easy to do. We could have something like this:

    public class DeferLoad : Control
    {
        private IControl _control;
        
        public static readonly StyledProperty<bool> LoadProperty = AvaloniaProperty.Register<DeferLoad, bool>(
            "Load");

        public bool Load
        {
            get => GetValue(LoadProperty);
            set => SetValue(LoadProperty, value);
        }
        
        [Content, TemplateContent]
        public object DeferredContent { get; set; }

        public IControl Control => _control;

        static DeferLoad()
        {
            LoadProperty.Changed.AddClassHandler<DeferLoad>((c, e) =>
            {
                if (e.NewValue is bool v)
                    c.DoLoad(v);
            });
        }
        
        protected override Size MeasureOverride(Size availableSize) 
            => LayoutHelper.MeasureChild(_control, availableSize, default);

        protected override Size ArrangeOverride(Size finalSize) 
            => LayoutHelper.ArrangeChild(_control, finalSize, default);


        private void DoLoad(bool load)
        {
            if((_control != null) == load)
                return;

            if (load)
            {
                _control = TemplateContent.Load(DeferredContent).Control;
                ((ISetLogicalParent)_control).SetParent(this);
                VisualChildren.Add(_control);
                LogicalChildren.Add(_control);
            }
            else
            {
                ((ISetLogicalParent)_control).SetParent(null);
                LogicalChildren.Clear();
                VisualChildren.Remove(_control);
                _control = null;
            }
            InvalidateMeasure();
        }
    }

Usage:

        <CheckBox x:Name="Load">Load</CheckBox>
        <example:DeferLoad Load="{Binding #Load.IsChecked}"> 
            <Button>Hello I'm deferred content</Button>
        </example:DeferLoad>

(you can just copy that class to your app right now, BTW)

Then XAML compiler could rewrite:

<Button x:Load="{Binding LoadButton}" >Test</Button>

into

<DeferredContentContainer>
  <Button x:Load="{Binding LoadButton}" >Test</Button>
</DeferredContentContainer>

which is also kinda easy to do. But we need to actually design the feature first. It would be really helpful to investigate how exactly this feature is implemented in UWP. Is placeholder element somehow being replaced with a loaded one or just adds it as its own child?

How does it work with x:Name and fields in codebehind?

@kekekeks : Hello, This class works well if the condition change but not when the DeferLoad is created (as the DefferedContent is not assigned yet. Is there any workaround for this ?
Tanks :)

@kekekeks
Copy link
Member

You can change the DeferredContent setter to invoke DoLoad if Load is already true.

@Whiletru3
Copy link
Contributor

You can change the DeferredContent setter to invoke DoLoad if Load is already true.

Perfect, thank you !

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

No branches or pull requests

6 participants