WPF Custom Markup Extensions
If you have ever used WPF would have come across Markup Extensions. These are the base of all data binding in WPF. The most ubiquitous one is {Binding}
but there is also {StaticResource}
, {DynamicResource}
{RelativeSource}
, etc.
From my experience it’s not a well known fact that you are free to create your own. The point of this blog is to start from nothing and have your own working extension. I will also cover some of the pitfalls that you can run into.
For this post we are going to create a custom localization extension to show text in multiple languages based on user choice. The end result will look like this.

Create a new WPF Application and open the MainWindow.xaml
file. Make the changes to the file to make it look like the following. Through the blog we will be creating each element.
<!-- file: MainWindow.cs --><Window x:Class="CustomMarkup.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomMarkup">
<!-- Where the strings are provided from -->
<local:StringProvider x:Name="StringProvider"
ActiveLanguage="{x:Static local:Languages.English}">
<StackPanel>
<!-- Using the custom markup to bind two text fileds -->
<TextBlock Text="{local:LocalizedBinding Greeting}"/>
<TextBlock Text="{local:LocalizedBinding ChooseLanguage}"/>
<!-- Allows the language to be switched -->
<ComboBox
SelectedIndex="0"
Text="{Binding ActiveLanguage, ElementName=StringProvider}"
ItemsSource="{Binding SupportedLanguages, ElementName=StringProvider}"/>
</StackPanel>
</local:StringProvider>
</Window>
There are a few unique elements inside of here.
StringProvider
: Stores the current active language and returns localized strings back.LocalizedBinding
: Used to look up the current language and fetch the localized version of a string based on an enum, returning the result.
To get started please add the following files to your project. This is mostly just boiler plate to get going.
Languages
Contains all the languages that we are supporting.
// file: Languages.cspublic enum Languages
{
English,
French,
German
}
StringProvider
This is used for two purposes, first is to store the current active language. Second is to provide a method for returning back a string based on a key and the current language. In a real application these would most likely be split up and also not hard coded.
// file: StringProvider.csnamespace CustomMarkup
{
public class StringProvider : ContentControl
{
private Languages m_activeLanguage;
public event Action<StringProvider>? LanguageChanged;
/// <summary>
/// Gets the supported languages that we have
/// </summary>
public IReadOnlyList<Languages> SupportedLanguages { get; } = Enum.GetValues<Languages>();
/// <summary>
/// Gets the currently active language
/// </summary>
public Languages ActiveLanguage
{
get => m_activeLanguage;
set
{
if (m_activeLanguage != value)
{
m_activeLanguage = value;
LanguageChanged?.Invoke(this);
}
}
}
/// <summary>
/// Gets the localized text for the given <see cref="AppStrings"/>
/// </summary>
public string GetString(string appString)
{
switch (ActiveLanguage)
{
// Greetings
case Languages.French when appString == "Greeting": return "Bonjour";
case Languages.English when appString == "Greeting": return "Hello";
case Languages.German when appString == "Greeting": return "Hallo";
// Choose
case Languages.French when appString == "ChooseLanguage": return "Choisissez votre langue";
case Languages.English when appString == "ChooseLanguage": return "Choose your Language";
case Languages.German when appString == "ChooseLanguage": return "Wähle deine Sprache";
default: return "!!NotFound!!";
}
}
}
}
Markup Implementation
With all the boilerplate out of the way we are going to start making some progression on the actual extension. Before we go on we need to cover a little ground.
Here is a rough example of how the binding we are creating will be used.
<StringProvider ActiveLanguage="English">
<Grid>
<TextBox Text="{LocalizedBinding HelloKey}"/>
</Grid>
</StringProvider>
The extension is going to do the following
- On load the TextBox (aka the
DepdencyObject
) will invokeProvideValue
- We will walk the hierarchy until we find a
StringProvider
(TextBox->Grid->StringProvider) - Using the value
"Greeting"
as a key we ask theStringProvider
for the localized version of it. - The extension creates a new instance of
Binding
and replaces the currentLocalizedBinding
. This new binding has a constant value of the localized string “Bonjour” if we were set to French. - We then subscribe to the
LanguageChanged
event in theStringProvider
and update the binding whenever it changes.

Don’t worry if all of these steps don’t make sense right now as we will cover them each below, this is just a high level view.
Now it’s time to start laying out the extension.
// file: LocalizedBinding.csusing System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
using System.Xaml;namespace CustomMarkup
{
public class LocalizedBinding : MarkupExtension
{
private StringProvider? m_provider;
private DependencyObject? m_targetObject;
private DependencyProperty? m_targetProperty; /// <summary>
/// Gets the unique key for the localized string
/// </summary>
public string Key { get; } public LocalizedBinding(AppStrings key)
{
Key = key;
}/// <summary>
/// Invoked once when the binding is created and never again, even
/// if the value changes. Once instace is created per binding
/// </summary>
public override object? ProvideValue(IServiceProvider serviceProvider)
{
// Fetch references to services we require
// IProvideValueTarget: Points at the object that has a
// property with a value being binded to.IProvideValueTarget? valueProvider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
// IRootObjectProvider: Allows getting a reference to
// root where the target it located. Usually of type
// Window or ContentControl.
IRootObjectProvider? rootProvider = serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider;
// If it's null we have nothing to do here
if (valueProvider == null) return null;
// Example Values for <TextBox Text=""/> Control
// TargetObject: TextBox
// TargetProperty: Text m_targetObject = valueProvider.TargetObject as DependencyObject;
m_targetProperty = valueProvider.TargetProperty as DependencyProperty;
object? result = GetDefalutValue(valueProvider);
// Case 1: Templates
if (IsShared(valueProvider, ref result)) return result;
// Case 2: Non-FrameworkElement types.
FrameworkElement? frameworkElement = GetFrameworkContext(valueProvider, rootProvider);
if (frameworkElement == null) return result;
// Case 3: Uninitialized Hierarchy
InitializeBinding(frameworkElement);
return result;
}
}
}
If you were to use the following code it would not compile. In the following steps we will implement each method. It’s has a bit excessive use of methods but it makes following along with this blog simpler.
Get Default Value
At the end of the day our binding must always return a real value otherwise when the binding is running (both while running the application and while view in visual studio) it will throw exceptions. This is where why we use the default value.
// file: LocalizedBinding.csprivate static object? GetDefalutValue(IProvideValueTarget provideValueTarget)
{
DependencyProperty? dependencyProperty = provideValueTarget.TargetProperty as DependencyProperty;
return dependencyProperty != null
? dependencyProperty.DefaultMetadata.DefaultValue
: Binding.DoNothing;
}
DependencyProperties
during creation must define a default value. So as long as we have a property we just return the value. There is a few edge cases where we don’t have a target. Mostly when it comes to templates. In this case we don’t want to do anything so we return Binding.DoNothing
. We can’t return null here because that would throw an exception when binding to a control that uses a value type.
Is Shared
This fixes an issue that will happen when you use your binding in a template. It’s important to know that the ProvideValue
function is called when the xaml is deserialized, then never again. When your binding is used inside a template it does not actually have a valid target for the TargetObject
since it’s a template that will be created at a later point. Since the target can’t be null WPF uses a shared instances of a private type called SharedDp
. We need to check for this and do special logic.
// file: LocalizedBinding.cspublic bool IsShared(object? targetObject, ref object? result)
{
if (targetObject == null)
{
return false;
}
Type targetType = targetObject.GetType();
// It's a bit garbage but SharedDP is internal
// so we can't compare types.
bool isShared = string.Equals("System.Windows.SharedDp", targetType.Name);
if (isShared)
{
result = new LocalizedBinding(Key);
return true;
}
return false;
}
In the section about GetDefaultValue
I mentioned that we always need to return a value that matches the property or execeptions will be thrown. When using templates there is an exception to the rule. Since ProvideValue
is called once we being loaded we need to to be called again when we have a real target. We do this by instead creating a copy of the current binding and returning that. WPF will then invoke ProvideValue again on the type created from the template.
It’s also very important that we return a copy because share one instance because each markup is only able to control one binding. Only the last one bound will have the correct value.
Get Framework Context
For our extension we need to walk the hierarchy of the components and find an instance of StringProvider
. The challenge we have is that when ProvideValue
is invoked the xaml hierarchy has not actually been loaded.

When we walk the hierarchy (as see in FindAncestorOfType)
use the Parent
property of the framework element. The problem is the ProvideValue
function is called way before that ever happens. So we need a way to delay until after the component has finished initializing. We do that by using the FrameworkElements
event Loaded
. To do this we must have a reference to an Framework element instance. That is what this function does.
// file: LocalizedBinding.csprivate FrameworkElement? GetFrameworkContext(IProvideValueTarget valueProvider, IRootObjectProvider? rootProvider)
{
FrameworkElement? frameworkElement = valueProvider.TargetObject as FrameworkElement;
if (frameworkElement == null && rootProvider != null)
{
frameworkElement = rootProvider.RootObject as FrameworkElement;
}
return frameworkElement;
}
The trick here is we are not guaranteed that the TargetObject
is a framework object. So in the rare cases where that happens we need to find a backup plan. We do this by using the RootObjectProvider
. This return back the root containing element that the target object is in. This is normally of type Window
or ContentControl
.

Initialize Binding
This is where we start to write up everything we built to make our binding work.
// file: LocalizedBinding.csprivate void InitializeBinding(FrameworkElement frameworkElement)
{
frameworkElement.Unloaded += ElementUnloaded;
if (frameworkElement.IsLoaded)
{
ConnectProvider(frameworkElement);
}
else
{
frameworkElement.Loaded += ElementLoaded;
}
}
As noted in the section above we need to wait until the element is initialized before we walk the hiearchy. Here we do a simple check, if not loaded we subscribe. Both paths will end up calling ConnectProvider
.
// file: LocalizedBinding.csprivate void ElementLoaded(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement frameworkElement)
{
frameworkElement.Loaded -= ElementLoaded;
ConnectProvider(frameworkElement);
}
}
We should also do the unload to free up resources.
// file: LocalizedBinding.cs
private void ElementUnloaded(object sender, RoutedEventArgs e)
{
if(sender is FrameworkElement frameworkElement)
{
frameworkElement.Unloaded -= ElementUnloaded;
if (m_provider != null)
{
m_provider.LanguageChanged -= OverrideBinding;
}
}
}
We need to make sure that we unsubscribe everything to subscribe to so we don’t leak memory.
// file: LocalizedBinding.csprivate void ConnectProvider(FrameworkElement frameworkElement)
{
m_provider = frameworkElement.FindAncestorOfType<StringProvider>();
if (m_provider == null)
{
throw new Exception("Unable to find string provider, please add one");
}
OverrideBinding(m_provider);
m_provider.LanguageChanged += OverrideBinding;
}
Here is where we use the utility function we created and walk up the tree until we find a string provider and store it as a local field. For the function I created a new static class for the helper.
//file: Utilities.cspublic static T? FindAncestorOfType<T>(this FrameworkElement? frameworkElement)
where T : FrameworkElement
{
while (frameworkElement != null)
{
if (frameworkElement is T asType)
{
return asType;
}
frameworkElement = frameworkElement.Parent as FrameworkElement;
}
return default;
}
The magic happens in the OverrideBinding
function. We need to call this function to update the value whenever our language chages so we also subscribe to to the LanguageChangedEvent
.
// file: LocalizedBinding.csprivate void OverrideBinding(StringProvider stringProvider)
{
Binding newBinding = new Binding()
{
Source = stringProvider.GetString(Key)
};
BindingOperations.ClearBinding(m_targetObject, m_targetProperty);
BindingOperations.SetBinding(m_targetObject, m_targetProperty, newBinding);
}
What we are doing is a bit tricky. We create new WPF Binding
instance, and we give it a constant string value as the source. We then replace the LocalizedBinding
that we are current in with the binding that we just created. This actually detaches our custom one and replaces the standard one.

The main reason this is done is becuase the Binding type is sealed and can’t be extended. It also has a bunch of logic that we would have to reimplement (which I don’t cover in this blog).
Go ahead and try it out and see it works for you.
This is my second attempt at writing a blog so let me know if it was useful to you and if there is intrest I can cover some more stuff.