WPF Custom Markup Extensions

It’s not pretty but it works
<!-- 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>
  • 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.

Languages

// file: Languages.cspublic enum Languages
{
English,
French,
German
}

StringProvider

// 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

<StringProvider ActiveLanguage="English">
<Grid>
<TextBox Text="{LocalizedBinding HelloKey}"/>
</Grid>
</StringProvider>
  1. On load the TextBox (aka the DepdencyObject ) will invoke ProvideValue
  2. We will walk the hierarchy until we find a StringProvider (TextBox->Grid->StringProvider)
  3. Using the value "Greeting" as a key we ask the StringProvider for the localized version of it.
  4. The extension creates a new instance of Binding and replaces the current LocalizedBinding . This new binding has a constant value of the localized string “Bonjour” if we were set to French.
  5. We then subscribe to the LanguageChanged event in the StringProvider and update the binding whenever it changes.
The binding asks for the value from the String Provider by walking the tree
// 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;
}
}
}

Get Default Value

// file: LocalizedBinding.csprivate static object? GetDefalutValue(IProvideValueTarget provideValueTarget)
{
DependencyProperty? dependencyProperty = provideValueTarget.TargetProperty as DependencyProperty;
return dependencyProperty != null
? dependencyProperty.DefaultMetadata.DefaultValue
: Binding.DoNothing;
}

Is Shared

// 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;
}

Get Framework Context

Initialize has not happened yet.
// 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;
}
Shows the different objects that would be returned

Initialize Binding

// file: LocalizedBinding.csprivate void InitializeBinding(FrameworkElement frameworkElement)
{
frameworkElement.Unloaded += ElementUnloaded;

if (frameworkElement.IsLoaded)
{
ConnectProvider(frameworkElement);
}
else
{
frameworkElement.Loaded += ElementLoaded;
}
}
// file: LocalizedBinding.csprivate void ElementLoaded(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement frameworkElement)
{
frameworkElement.Loaded -= ElementLoaded;
ConnectProvider(frameworkElement);
}
}
// 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;
}
}
}
// 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;
}
//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;
}
// 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);
}
The Localized Binding being replaced by the standard Binding.

--

--

--

I am a tools and pipeline developer currently working at EA Motive. I have a passion for understanding how technology works and break systems apart.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

WIP — Some notes on development and deployment-operation cycles

Open Source Video Conferencing

each other neither one listening but waiting for the other person to stop talking so they can…

Guide to Writing Clean Code

How to Scale SaaS Product Features Development with EaaS

Enterprise Architecture Management and Agile Software Development: Paradox or a fruitful…

[PDF] ???????????????????? Christopher Barry Cox, Peter D. Moore, Richard J. Ladle ?, ??? ?

How And Why To Use Enums In Ruby On Rails

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Byronmayne

Byronmayne

I am a tools and pipeline developer currently working at EA Motive. I have a passion for understanding how technology works and break systems apart.

More from Medium

Build a chat app using ASP.NET

Effortlessly Renew API Tokens in ASP.NET 6.0

Bayfront avenue, future world @ artscience museum, singapore

Blazor Wolf In COM Sheep’s Clothing — Ship Blazor To Excel With ActiveX

【C#】Creational Patterns Factory Mode