-
Notifications
You must be signed in to change notification settings - Fork 813
Description
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()-
IXamlServiceProvider (System.IServiceProvider)
-
IProvideValueTarget (System.Windows.Markup.IProvideValueTarget)
-
IRootObjectProvider (System.Xaml.IRootObjectProvider)
-
IUriContext (System.Windows.Markup.IUriContext)
-
IXamlTypeResolver (System.Windows.Markup.IXamlTypeResolver)
-
-
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.