Skip to content

Proposal: API for providing parser context to custom markup extensions #741

@evelynwu-msft

Description

@evelynwu-msft

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’s 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature proposalNew feature proposalneeds-winui-3Indicates that feature can only be done in WinUI 3.0 or beyond. (needs winui 3)team-MarkupIssue for the Markup team

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions