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

Conditional XAML #1434

Open
worldbeater opened this issue Mar 9, 2018 · 21 comments
Open

Conditional XAML #1434

worldbeater opened this issue Mar 9, 2018 · 21 comments

Comments

@worldbeater
Copy link
Contributor

worldbeater commented Mar 9, 2018

To reduce load time or to increase layout performance one may need to conditionally detach layout controls from visual tree. For example, when layout part loads images from the Net, but a user has disabled image loading in the application settings, XAML layout does not need to load the view part responsible for image showing.

In Angular, there is a NgIf directive that solves this issue. So please, take a look: https://angular.io/api/common/NgIf

So, maybe Avalonia framework should have such control in its standard library? Something named AvalonIf that we can use in our application like this:

<AvalonIf Value="{Binding ShouldLoad, Mode=OneWay}">
    <Image Source="{Binding Image, Mode=OneTime}"/>
</AvalonIf>

Thanks!

@notanaverageman
Copy link
Contributor

I think this can be achieved easily by binding to Visibility property, no need for a separate control.

@worldbeater
Copy link
Contributor Author

I think this can be achieved easily by binding to Visibility property, no need for a separate control.

Changing Visibility to collapsed or hidden only hides the control so a user does not see it, but it doesn't detach it from visual tree, so images will continue loading in background and controls will get eagerly initialized.

@grokys
Copy link
Member

grokys commented Mar 9, 2018

Interesting. I've never needed such a thing, perhaps because I tend to use MVVM, so if images are disabled then that would be done at the ViewModel level.

I'm not sure if this would be a common enough case to include the control in the core library. Do you have any more examples?

@ForNeVeR
Copy link
Contributor

ForNeVeR commented Mar 9, 2018

I think that something like that could be naturally achieved using ContentControl and DataTemplates: set the DataTemplate to some dummy value when you don't want to see the data (it won't even bother to create the controls from the "wrong" DataTemplate, and that's exactly what you need). It will require a bit of MVVM programming or TemplateSelector though.

@worldbeater
Copy link
Contributor Author

worldbeater commented Mar 9, 2018

I tend to use MVVM, so if images are disabled then that would be done at the ViewModel level.

But how to implement this using MVVM? As for now, I read settings using Load command and expose a property named ImagesEnabled from my ViewModel — so the UI knows if images are disabled and can easily detach unnecessary controls off using such conditional XAML control.

Are there any other ways of doing this? I thought of spoiling image urls — but this is a dirty solution. Another way is to write a custom control inherited from Image that won't load content until a special flag is set — but this solution is not universal — what if I need square images, round images, triangle images (Illuminati will appreciate), or videos.

Creating content controls with several DataTemplates seems to be a working solution (thanks, @ForNeVeR), but a bit complicated for such a simple task as conditionaly showing/hiding elements!

I'm not sure if this would be a common enough case to include the control in the core library. Do you have any more examples?

Frankly speaking, I could hardly imagine any other cases where conditional XAML will be the only way to go, so including this feature in the core library may not be really necessary @grokys! But in the future, it can potentially simplify one's codebase. :)

@grokys
Copy link
Member

grokys commented Mar 9, 2018

Note: First let me say that I've never run into this particular case so I could be missing something!

Ok, so the way I would do it would just be to expose a null for the image source/URL from the view model when ImagesEnabled == false. If you look at the source for Image you can see that the control pretty much does nothing when Source == null so there should be no overhead other than the control being added to the tree.

And note that if you were to use an <AvaloniaIf> control, that control would still be added to the tree, so you're back to the same place as just adding an <Image> with a null Source. If we want to be able to not create a particular sub-tree if a condition is set, then that would require XAML support I think.

If you have other examples though, please let me know!

@maxkatz6
Copy link
Member

UWP has build in x:DeferLoadStrategy attribute for lazy loading and x:Load attribute (Creators Update+) for loading and unloading with binding, which can replace UwpIf/AvaloniaIf

<Grid>
    <Image x:Name="MyImage" 
           x:DeferLoadStrategy="Lazy" 
           Source="{Binding Image, Mode=OneTime}"
           Visibility="Collapsed"/>
    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup>
        <VisualState>
          <VisualState.StateTriggers>
            <StateTrigger IsActive="{Binding ShouldLoad, Mode=OneWay}"/>
          </VisualState.StateTriggers>
          <VisualState.Setters>
            <Setter Target="MyImage.Visibility"
                    Value="Visible"/>
          </VisualState.Setters>
        </VisualState>
    </VisualStateManager.VisualStateGroups>
</Grid>

Or

    <Image x:Load="{Binding ShouldLoad, Mode=OneWay}" 
           Source="{Binding Image, Mode=OneTime}"/>

@maxkatz6
Copy link
Member

Also UWP has Conditional XAML using namespaces. But it seems to solve other problems.

@robloo
Copy link
Contributor

robloo commented Dec 20, 2020

Conditional XAML is a much wider topic necessary for XAML re-use between frameworks. I definitely agree that the UWP conditional XAML approach should be used in Avalonia. It was also adopted by Uno and allows platform-specific controls and UI.

@maxkatz6
Copy link
Member

Latest master supports OnPlatform extension (works similarly to MAUI one, but completely compiled).

@robloo
Copy link
Contributor

robloo commented Nov 22, 2022

@maxkatz6 I'm not sure OnPlatform can actually be used to swap out controls on different platforms? Seems to be fine to do something really simple like change colors but to use it to control the visual tree it seems inadequate. Does Avalonia XAML allow something like switching between a DataGrid and ListBox based on macOS or Android?

@maxkatz6
Copy link
Member

@robloo, yes, it does.

<Panel>
   <OnPlatform>
       <OnPlatform.Default>
            <DataGrid />
       </OnPlatform.Default>
       <OnPlatform.Android>
            <ListBox />
       </OnPlatform.Android>
   </OnPlatform>
</Panel>

It will be translated into IL code like:
Panel.Children.Add(IsOnAndroid() ? new ListBox() : new DataGrid())
which means only one "branch" will be executed and intialized.

This, or users can use OnFormFactor which has Mobile or Default options, which is closer to what you asked.

@maxkatz6
Copy link
Member

It also works with resource dictionaries, generics, <On Options='Windows, macOS' /> combined syntax and more: https://github.com/AvaloniaUI/Avalonia/blob/master/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs
Custom extensions with custom conditions can also be created.

I will write a documentation about it closer to release.

@robloo
Copy link
Contributor

robloo commented Nov 23, 2022

I see, thanks for the details. I was looking at Maui and it looks like it has a bit nicer syntax in this case but is similar. Perhaps you tried to generalize it more.

https://github.com/microsoft/dotnet-podcasts/blob/00be1bb25b6940fee77f0e7b711a04dea2358ced/src/Mobile/Controls/HeaderControl.xaml#L9-L32

@workgroupengineering
Copy link
Contributor

I see, thanks for the details. I was looking at Maui and it looks like it has a bit nicer syntax in this case but is similar. Perhaps you tried to generalize it more.

https://github.com/microsoft/dotnet-podcasts/blob/00be1bb25b6940fee77f0e7b711a04dea2358ced/src/Mobile/Controls/HeaderControl.xaml#L9-L32

MAUI also supports Trim OnPlatform.

in the example:

@robloo, yes, it does.

<Panel>
   <OnPlatform>
       <OnPlatform.Default>
            <DataGrid />
       </OnPlatform.Default>
       <OnPlatform.Android>
            <ListBox />
       </OnPlatform.Android>
   </OnPlatform>
</Panel>

It will be translated into IL code like: Panel.Children.Add(IsOnAndroid() ? new ListBox() : new DataGrid()) which means only one "branch" will be executed and intialized.

This, or users can use OnFormFactor which has Mobile or Default options, which is closer to what you asked.

When the RuntimeIdentifier is set it removes platforms not supported by the runtime.

In the previous example if the RuntimeIdentifier is set to net6.0-android the compiled xaml is:

Panel.Children.Add(new ListBox())

@robloo
Copy link
Contributor

robloo commented Nov 23, 2022

@workgroupengineering @maxkatz6 This is an important point. Ignoring the syntax differences with Maui for now, Maui would not require the DataGrid nuget package on platforms it isnt used. Avalonia will require it... which doesn't make a lot of sense. This should probably be adjusted to more closely follow Maui.

@maxkatz6
Copy link
Member

@robloo this "On" syntax is also supported in avalonia. https://github.com/AvaloniaUI/Avalonia/blob/master/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs#L296-L303

@workgroupengineering the main point of this removing is to avoid creation of unneeded branches in the code. Avalonia already does it. Also, MAUI example works with TargetFramework and not runtime identifier, and it won't work well with libraries, which target, let's say, "net6.0" to support any platform. In Avalonia with XamlX we have possibility to inject actual "switch" expression in the IL code.

But it won't remove the branch completely, so package dependency would still be required, as @robloo pointed.

@robloo
Copy link
Contributor

robloo commented Nov 23, 2022

@maxkatz6

this "On" syntax is also supported in avalonia.

Yes, I saw it was supported however the syntax is different as shown below. Maui is much more obvious in the common case of switching controls per-platform. However, Avalonia appears to be a bit more generically useful (I'm not sure how/where options are defined though).

Maui

<OnPlatform x:TypeArguments="View">
    <On Platform="UWP, macOS">
          <Label />
    </On>
    <On Platform="Android,iOS">
        <Grid />
    </On>
</OnPlatform>

Avalonia

<local:OptionsMarkupExtension>
    <On Options='OptionA, OptionB'>
        <SolidColorBrush Color='#ff506070' />
    </On>
    <On Options=' OptionNumber '>
        <SolidColorBrush Color='#000' />
    </On>
</local:OptionsMarkupExtension>

But it won't remove the branch completely, so package dependency would still be required, as @robloo pointed.

For the first version that is probably fine. Long term it's going to be a problem though. Packages/controls will diverge in some cases between Desktop and Mobile.

@maxkatz6
Copy link
Member

For the first version that is probably fine. Long term it's going to be a problem though. Packages/controls will diverge in some cases between Desktop and Mobile.

In case of DataGrid it's probably unavoidable anyway. For two reasons:

  • Control theme needs to be included in the app. Devs can possibly include it inside of the OnPlatform branch, but it doesn't sound like a nice experience. There is no possibility in .NET Runtime now to initialize some code (control themes in our case) only if specific type was used in the app. See [API Proposal]: MakeWeakTypeReference as trimming indicator dotnet/runtime#74307 for more details.
  • With custom grouping or sorting devs need to instantiate special collections from the DataGrid assembly. So, it won't be removed either.

So that's definitely a long-term problem, also partially blocked by the runtime.
Ideally majority of DataGrid assembly could be just trimmed out, even when library is referenced. But this control has too much reflection in its blood...

@maxkatz6
Copy link
Member

maxkatz6 commented Nov 23, 2022

Yes, I saw it was supported however the syntax is different as shown below. Maui is much more obvious in the common case of switching controls per-platform. However, Avalonia appears to be a bit more generically useful (I'm not sure how/where options are defined though).

I don't see what's a difference between these two? Except "Platform" was renamed to "Options" to be more general. And typearguments are optional.
Keep in mind that name "local:OptionsMarkupExtension" is just a test extension, just to prove that devs can create their own OnPlatform alternatives. In real same would look like:

<OnPlatform> <!--  x:TypeArguments="View" can be added, but not necessary -->
    <On Options="Windows, macOS">
          <Label />
    </On>
    <On Options="Android,iOS">
        <Grid />
    </On>
</OnPlatform>

@tivewsalaeharad
Copy link

tivewsalaeharad commented Dec 15, 2022

Speaking about conditional content, I can mention TemplatedControl with different StyleSelectors.

ValueRenderer.axaml.cs

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;

namespace MyNamespace;

public class ValueRenderer : TemplatedControl
{
    public static readonly DirectProperty<ValueRenderer, int> MyReadOnlyParamProperty = AvaloniaProperty.RegisterDirect<ValueRenderer, int>(
        "MyReadOnlyParam", o => o.MyReadOnlyParam);

    public int MyReadOnlyParam => MyParam.Length;

    public static readonly StyledProperty<string> MyParamProperty = AvaloniaProperty.Register<ValueRenderer, string>(
        nameof(MyParam));

    public string MyParam
    {
        get => GetValue(MyParamProperty);
        set => SetValue(MyParamProperty, value);
    }
}

ValueRenderer.axaml

<Styles xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:controls="using:MyNamespace">
    <Design.PreviewWith>
        <StackPanel>
            <controls:ValueRenderer MyParam="myval"/>
            <controls:ValueRenderer MyParam="myva"/>
            <controls:ValueRenderer MyParam="myv"/>
        </StackPanel>
    </Design.PreviewWith>

    <Style Selector="controls|ValueRenderer">
        <!-- Set Defaults -->
        <Setter Property="Template">
            <ControlTemplate>
                <TextBlock Text="Templated Control (other letter amount)" />
            </ControlTemplate>
        </Setter>
    </Style>
    <Style Selector="controls|ValueRenderer[MyReadOnlyParam=3]">
        <!-- Set Defaults -->
        <Setter Property="Template">
            <ControlTemplate>
                <TextBlock Text="Templated Control (thee letters)" />
            </ControlTemplate>
        </Setter>
    </Style>
    <Style Selector="controls|ValueRenderer[MyReadOnlyParam=4]">
        <!-- Set Defaults -->
        <Setter Property="Template">
            <ControlTemplate>
                <TextBlock Text="Templated Control (four letters)" />
            </ControlTemplate>
        </Setter>
    </Style>
</Styles>

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

9 participants