Microsoft-ui-xaml: Proposal: API for providing parser context to custom markup extensions

Created on 22 May 2019  路  21Comments  路  Source: microsoft/microsoft-ui-xaml

Proposal: API for providing parser context to custom markup extensions

Summary

Custom markup extensions allow developers to create their own markup extensions (by deriving from the base class Microsoft.UI.Xaml.Markup.MarkupExtension and overriding the ProvideValue() method). However, unlike WPF and Silverlight, UWP Xaml鈥檚 implementation does not provide information about the parser context to custom markup extensions, which limits much of their potential usefulness.

Rationale

  • Will help close the gap between UWP and WPF
  • Very highly requested improvement from developers
  • Facilitates sharing code between WPF and UWP apps

Scope

| Capability | Priority |
| :---------- | :------- |
| This proposal will allow developers to share their custom markup extension core logic between UWP and WPF | Must |
| This proposal will mirror the existing .Net interfaces as closely as possible | Must |
| This proposal will enable custom markup extension scenarios for UWP that are not possible in WPF | Won't |

Important Notes

Overview of markup extensions

A MarkupExtension is a builder, like a .Net StringBuilder, which allows you to create, modify, and then retrieve a value.

Xaml markup understands MarkupExtensions; it understands to use the built ("provided") value, and it lets MarkupExtensions be created using { } shorthand syntax.

For example, given this markup extension:

public class CurrentDate : MarkupExtension
{
    public string Format { get; set; } = "";

    protected override object ProvideValue()
    {
        return DateTime.Now.ToString(Format);
    }
}

You can do this in XAML markup:

<TextBlock>
    Today is:
    <Run Text="{local:CurrentDate Format=d}" />
</TextBlock>

On an en-US machine, this will produce something like:

Today is: 5/22/2019

The difference between WPF and UWP Xaml

What WPF has, and UWP Xaml lacks, is a parameter on the ProvideValue() method. That is, the WPF MarkupExtension differs from Xaml's:

protected override object ProvideValue(IServiceProvider serviceProvider)
{
    return DateTime.Now.ToString(Format);
}

IServiceProvider is roughly equivalent to QI'ing for an interface at runtime in COM:

public interface IServiceProvider
{
    public object GetService(Type serviceType);
}

What's being added

  • New overload for MarkupExtension.ProvideValue, mirroring WPF's and Silverlight's System.Windows.Markup.ProvideValue(IServiceProvider)

  • New interfaces mirroring the .Net interfaces most commonly used with ProvideValue()

  • New class, ProvideValueTargetProperty, to provide information about the target property of the markup extension

    • In WPF/Silverlight, the IProvideValueTarget.TargetProperty property will return either a DependencyProperty or a PropertyInfo (if the target property is a CLR property). WinRT does not have an equivalent of PropertyInfo, however, due to lack of reflection. Rather than invent a wholesale replacement for PropertyInfo, we opted to instead add a class that contains just enough information to identify the property and name it in such a way that it is scoped to just the UWP XAML framework.
  • Nuget package to provide boilerplate adapters (implemented as extension methods on the new interfaces, a la the System.Runtime.WindowsRuntime Nuget package) converting from the UWP XAML interfaces to their .Net counterparts

    • The purpose of this is to simplify sharing of code between UWP and WPF/Silverlight applications by relieving developers of the need to write uninteresting boilerplate

API Usage Examples

IXamlServiceProvider interface

Gets the service object of the specified type.

Definition:

interface IXamlServiceProvider
{
    Object GetService(Windows.UI.Xaml.Interop.TypeName type);
};

The following example shows a class that retrieves the current date:

public class MyDateFormatter
{
    public string GetDate()
    {   
        return DateTime.Now.ToString("d");
    }
}

A class can return this as a service:

public class MyServiceProvider : IXamlServiceProvider
{
    public object GetService(Type serviceType)
    {
        if (serviceType == typeof(MyDateFormatter))
        {
            return new MyDateFormatter();
        }
        else
        {
            return null;
        }
    }
}

Other code can use IXamlServiceProvider to retrieve it:

string GetDateFromProvider(MyServiceProvider serviceProvider)
{
    var myDateFormatter = (MyDateFormatter)serviceProvider.GetService(typeof(MyDateFormatter));

    return myDateFormatter.GetDate();
}

IProvideValueTarget

Provides a target object and property.

Definition:

interface IProvideValueTarget
{
    Object TargetObject { get; };
    Object TargetProperty { get; };
};

Xaml MarkupExtensions are offered this interface via the IXamlServiceProvider parameter. The target object/property are the instance and property identifier that the markup extension is being set on.

The following example shows a custom markup extension whose provided value changes based on the specific property being targeted by the custom markup extension. In this case, the developer wants something that will automatically use an AcrylicBrush for the Control.Background or Border.Background properties, but uses a SolidColorBrush for all other properties.

C#

public class BrushSelectorExtension : MarkupExtension
{
    public Color Color { get; set; }
    protected override object ProvideValue(IXamlServiceProvider serviceProvider)
    {
        Brush brushToReturn = new SolidColorBrush() { Color = Color }; ;
        var provideValueTarget = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;

        if (provideValueTarget.TargetProperty is ProvideValueTargetProperty targetProperty)
        {
            if (targetProperty.Name == "Background" && (targetProperty.DeclaringType == typeof(Control) || targetProperty.DeclaringType == typeof(Border)))
            {
                brushToReturn = new AcrylicBrush() { TintColor = Color, TintOpacity = 0.75 };
            }
        }

        return brushToReturn;
    }
}

XAML

<StackPanel>
    <Button Foreground="{local:BrushSelector Color=Blue}" 
            Background="{local:BrushSelector Color=Gold}">
        Go bears!
    </Button>
    <Rectangle x:Name="SolidColor" Fill="{local:BrushSelector Color=Green}" />
</StackPanel>

IRootObjectProvider interface

Describes a service that can return the root object of markup being parsed.

Definition:

interface IRootObjectProvider
{
    Object RootObject { get; };
};

Xaml MarkupExtensions are offered this interface via the IXamlServiceProvider parameter. This is the object at the root of the input markup.

For example, with this markup extension:

public class TestMarkupExtension : MarkupExtension
{
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var target = serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider;

        return target.RootObject.ToString();
    }
}

The TextBlock in this markup will display "App1.MainPage":

<Page x:Class="App52.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App52"
      Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid>
        <TextBlock Text="{local:TestMarkupExtension}" />
    </Grid>
</Page>

IUriContext interface

Provided by the Xaml loader to MarkupExtensions to expose the base URI of the markup being loaded

Definition:

interface IUriContext

{
    Windows.Foundation.Uri BaseUri { get; };
};
  • [ ] Find an example of how this is useful

Scenario calling for use of adapters

A developer wants to share the core logic of her custom markup extension between UWP and WPF, so she downloads the Nuget package containing the adapter that converts between the .Net and UWP versions of the IServiceProvider interface.

C#

public class LocalizationExtension : Microsoft.UI.Xaml.Markup.MarkupExtension
{
    protected override object ProvideValue(Microsoft.UI.Xaml.IXamlServiceProvider serviceProvider)
    {
        var dotNetServiceProvider = serviceProvider.AsDotNetIServiceProvider();
        var dotNetProvideValueTarget = dotNetServiceProvider.GetService(typeof(System.Windows.Markup.IProvideValueTarget)) as System.Windows.Markup.IProvideValueTarget;

        if (dotNetProvideValueTarget != null)
        {
            return SharedLibrary.LocalizationExtensionProvideValueCore(dotNetProvideValueTarget);
        }

        return null;
    }
}

public class SharedLibrary
{
    public static object LocalizationExtensionProvideValueCore(System.Windows.Markup.IProvideValueTarget provideValueTarget)
    {
        // do magic here
        return new Object();
    }
}

Interface Definition

IDL for new XAML APIs

namespace Microsoft.UI.Xaml
{
    [webhosthidden]
    interface IXamlServiceProvider
    {
        Object GetService(Windows.UI.Xaml.Interop.TypeName type);
    };
}

namespace Microsoft.UI.Xaml.Markup
{
    [webhosthidden]
    interface IProvideValueTarget
    {
        Object TargetObject{ get; };
        Object TargetProperty{ get; };
    };

    [webhosthidden]
    interface IRootObjectProvider
    {
        Object RootObject{ get; };
    };

    [webhosthidden]
    interface IUriContext
    {
        Windows.Foundation.Uri BaseUri;
    };

    [webhosthidden]
    interface IXamlTypeResolver
    {
        Windows.UI.Xaml.Interop.TypeName Resolve(String qualifiedTypeName);
    };

    [webhosthidden]
    [constructor_name("Microsoft.UI.Xaml.Markup.IMarkupExtensionFactory")]
    [default_interface]
    [interface_name("Microsoft.UI.Xaml.Markup.IMarkupExtension")]
    unsealed runtimeclass MarkupExtension
    {
        [method_name("CreateInstance")] MarkupExtension();

        [overridable_name("Microsoft.UI.Xaml.Markup.IMarkupExtensionOverrides")]
        {
            overridable Object ProvideValue();
            overridable Object ProvideValue(Microsoft.UI.Xaml.IXamlServiceProvider serviceProvider);
        }
    };

    [webhosthidden]
    runtimeclass ProvideValueTargetProperty
    {
        ProvideValueTargetProperty();
        String Name{ get; };
        Windows.UI.Xaml.Interop.TypeName Type{ get; };
        Windows.UI.Xaml.Interop.TypeName DeclaringType{ get; };
    };
}

API for Nuget adapter

namespace System.Runtime.InteropServices.WindowsRuntime
{
    public static class XamlParserServiceExtensions
    {
        public static System.IServiceProvider AsDotNetIServiceProvider(this Microsoft.UI.Xaml.IXamlServiceProvider xamlServiceProvider) 
        { 
            return null;
        }
    }
}

Open Questions

Naming of IXamlServiceProvider

The preference is to have this interface be named IServiceProvider to match the .Net interface's name. However, there are potential complications due to the existence of an IServiceProvider COM interface exposed by servprov.h. While this isn't expected to be a problem for most developers migrating to WinUI 3.0, a number of components within the Windows code base ultimately include both servprov.h and XAML headers which leads to ambiguity if the interface added by this proposal is also named IServiceProvider.

feature proposal needs-winui-3 team-Markup

Most helpful comment

The current plan is to make this API available through WinUI 3.0. I think the chances are low that this will be part of the 20H1 SDK as well.

All 21 comments

This looks really good, team. Thank you for taking the time to capture it and to allow proper community feedback.

My only question is why name it IXamlServiceProvider instead of the framework System.IServiceProvider? It would seem that everything else is a 1:1 consistent/expected mapping with the exception of this service, which of course stands out as System.IServiceProvider is a v1.0 component that is utilized quite a bit throughout .NET and supporting packages.

There is an unfortunate name collision with a COM interface also named IServiceProvider that is defined in the root namespace. The IXamlServiceProvider name is primarily just a placeholder, but I'm not confident that we'll be able to use IServiceProvider as the final name.

Is the "root" namespace System? :) The whole point of namespaces is to allow the same name of a component in two different places, after all.

Is there no magic mapping that can occur between an API for this to work in COM? I guess I haven't seen this been an issue before. It would seem at the least we could preserve the integrity/consistency/history/legacy/etc. for .NET and "put the ugly" in COM -- it's sort of its thing, after all. 馃槅

@alwu-msft: I don't see an IServiceProvider in any WinRT namespace (or in COM's global ns). The link you cited points to an API in Internet Explorer. Searching on docs.microsoft.com returns several results, but they all seem to be product specific, like VS, SQL, MEF.

There's a large amount of code in the Windows sources that includes both servprov.h (which defines the IE IServiceProvider interface I linked to) and the XAML headers, and also has a using Windows::UI::Xaml statement. The result is that IServiceProvider ends up being ambiguous in a number of places.

This may all be moot with WinUI 3.0 as individual components will be moving to WinUI 3.0 at their own pace, or possibly even not at all. I'll update the proposal to explicitly note that IXamlServiceProvider is a placeholder (and the reason why) and that there's a preference for using IServiceProvider if possible.

@Mike-EEE In case you haven't noticed it yet, seems like this work item was done in Win 10 Preview SDK 18950!

Wow, @Felix-Dev that's pretty cool! Thank you for the update. @alwu-msft are you able to officially confirm/comment?

Hey @Felix-Dev and @Mike-EEE, that's actually an error in the blog post. We're not certain how it snuck into the change list, but the API is not available in the Insider Preview SDK. Sorry for getting your hopes up. :(

I noticed it was missing in the preview SDK 18956 change list and thought it was excluded by a mistake...turns out it was previously included by mistake...馃槬

@alwu-msft Of course, question now is, is there a chance to see this API added to the SDK for the 20H1 update?

The current plan is to make this API available through WinUI 3.0. I think the chances are low that this will be part of the 20H1 SDK as well.

@alwu-msft I really was only asking for an ETA here, good to see that it _might_ arrive in a not so far away future!

Also not in Windows 10 SDK Preview Build 18965.

@Mike-EEE If I'm looking at the code correctly, this is WPF only, yes?

Btw, I was in love with https://github.com/Alex141/CalcBinding , when I was doing WPF. That stuff rocks big time!

@Mike-EEE If I'm looking at the code correctly, this is WPF only, yes?

Which code, @jtorjo? 馃榿 To be sure, WPF contains the ideal blueprint of a markup extension, of which OpenSilver has successfully replicated it and is available now for development.

As for this issue, it has been nearly ten years of requests, votes, and petitioning from the UWP group for this feature and it still appears to be a confounding, perplexing problem for the entire division at large to tackle.

OpenSilver works ubiquitously in the browser, or to be more specific WebAssembly via Blazor (from what I understand). It is not WPF per se but works on all devices that can run WPF, assuming they run it on a modern HTML5-capable browser (which are pretty common -- nay, ubiquitous! -- these days).

Also, I did take a look at that CalcBinding and indeed it brings back the feels. 馃槉

@Mike-EEE I meant, the OpenSilver code. By looking at what namespaces it's using, it can't support UWP :D

Hehe, CalcBinding rocks big time. I did a lot of cursing when I couldn't port it to UWP, so what I have now is a workaround (on UWP).

Ah apologies for the confusion @jtorjo. I was pointing out the fact that there is another project that has basically accomplished and released what this group has not been able to achieve after nearly a decade of effort. Since it works in all modern browsers, that means it works in a far greater install base: approximately 3.5-4 billion devices when you combine Windows, Apple, and Droid devices as opposed to ~800 million Windows 10 installs where UWP only works.

So yeah, that's sort of me giving up on the idea that markup extensions will ever be successfully replicated on a Windows SDK. Even if it is, why bother since there is already a working, ubiquitous offering that already does the magic and works everywhere.

So yeah, that's sort of me giving up on the idea that markup extensions will ever be successfully replicated on a Windows SDK. Even if it is, why bother since there is already a working, ubiquitous offering that already does the magic and works everywhere.

@Mike-EEE Well, yes, but that clearly would not play nice with UWP (WinUI, for that matter).

Having said that, if there's enough interest, I may publish my code, which kind of does a simple equivalent of CalcBinding library above. You would be able to do stuff like this:

public class ui_media : calc_binding, INotifyPropertyChanged
{
...
    public bool is_even => expression_value(index % 2 == 0, "index", PropertyChanged);
    public bool is_odd => expression_value(index % 2 == 1, "index", PropertyChanged);
    public bool is_on_edge => expression_value(is_on_left_edge || is_on_right_edge, "is_on_left_edge is_on_right_edge", PropertyChanged);
    public bool is_not_on_edge => expression_value(!is_on_left_edge && !is_on_right_edge, "is_on_left_edge is_on_right_edge", PropertyChanged);

    public CoreCursorType cursor => expression_value(is_on_left_edge || is_on_right_edge ? CoreCursorType.SizeWestEast : CoreCursorType.Hand , "is_on_left_edge is_on_right_edge", PropertyChanged);
}

You can then bind stuff in xaml,

<Rectangle Fill="{ThemeResource LightBrush}" binding:vc_dep.Visible="{Binding is_even}"/>
...
<Grid Opacity="{Binding image_opacity}">

Possibly a side note, just offering some food for thought here. I feel that with all the new XAML features being added recently to UWP, such as the new compiled bindings, there's some overlaap in functionality that should probably made clearer for developers to help guide their choice of tools to use.

I'm saying this because, for instance, I noticed that one of the examples in the original post can actually be entirely rewritten using the new (and faster) x:Bind syntax, specifically this one:

// Original post, with markup extension
public class CurrentDate : MarkupExtension
{
    public string Format { get; set; } = "";

    protected override object ProvideValue()
    {
        return DateTime.Now.ToString(Format);
    }
}
<!--Usage-->
<TextBlock Text="{local:CurrentDate Format=d}"/>

Which can be rewritten today as simply:

xmlns:system="using:System"

<TextBlock Text="{x:Bind system:DateTime.Now.ToString('d', x:Null)}"/>

Much more compact, with no new types, and also much faster and more efficient.

This is all to say that, while I agree that many of the features listed in the original post are not available today at all, I think it'd be a good idea to also make it clear in the docs where a markup extension can fully be rewritten/implemented as just a compiled binding to a static function, where it makes sense to do so, and why it might be better for developers to choose that route (namely, to have compile-time safety, faster performance and less memory usage). 馃槃

@alwu-msft is this in any of the WinUI 3 previews yet? Or will it be part of Preview 3?

@michael-hawker It will be part of Preview 3.

Was this page helpful?
0 / 5 - 0 ratings