Binding WPF ComboBox to enum and hiding certain values - wpf

I have a WPF combobox that is bound to an enum like this:
<Window.Resources>
<local:EnumDescriptionConverter x:Key="enumDescriptionConverter"/>
<ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="cityNamesDataProvider">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:MyModel+CityNamesEnum"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
<ComboBox x:Name="cityNameComboBox" ItemsSource="{Binding Source={StaticResource cityNamesDataProvider}}" SelectionChanged="cityNameComboBox_SelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource enumDescriptionConverter}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
The enum that I'm binding to has description attributes and looks like this:
public enum CityNamesEnum
{
[Description("New York City")]
NewYorkCity,
[Description("Chicago")]
Chicago,
[Description("Los Angeles")]
LosAngeles
}
I don't always want to display each enum value. Is it possible to toggle the visibility of one or more of the enum values? If these were ComboBoxItems, I think I could simply set the .Visibility property to hidden but since they're enum values, I'm not sure if this is possible. Does anyone know?

Why not just create a normal C# method which does the filtering for you and then have the ObjectDataProvider point to that method instead
static method IEnumerable<CityNamesEnum> MyFilter() {
yield return CityNames.NewYorkCity;
yield return CityNames.Chicago;
}
XAML
<ObjectDataProvider
MethodName="MyFilter"
ObjectType="{x:Type local:TheType}"
x:Key="cityNamesDataProvider">
</ObjectDataProvider>

The example here is applied to a ComboBox, but will work all the same for any Enum Binding. It will however do exactly what you want: Hide Enum values that don't have a Description. This also provides an easy Binding method. (as well as sorting them by Index though, can be easily changed in the function "SortEnumValuesByIndex()")
Origin:
This anwser is based on the original work of Brian Lagunas' EnumBindingSourceExtension + EnumDescriptionTypeConverter.
I have made modifications for it to better suit my needs.
What I changed:
Extended the Enum class with a boolean function that checks if the EnumValue has the [Description] attribute or not
public static bool HasDescriptionAttribute(this Enum value)
{
var attribute = value.GetType().GetField(value.ToString())
.GetCustomAttributes(typeof(DescriptionAttribute), false)
.FirstOrDefault();
return (attribute != null);
}
Modified Brian's "ConvertTo()" function in "EnumDescriptionTypeConverter" to return "null" in case the [Description] attribute was not applied
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
...
// Original: return ((attributes.Length > 0) && (!String.IsNullOrEmpty(attributes[0].Description))) ? attributes[0].Description : value.ToString();
return ((attributes.Length > 0) && (!String.IsNullOrEmpty(attributes[0].Description))) ? attributes[0].Description : null;
}
Modified Brian's "ProvideValue()" function in "EnumBindingSourceExtension" by editing it's return value by calling my own function
ProvideValue(IServiceProvider serviceProvider)
{
...
// Original: return enumValues
return SortEnumValuesByIndex(enumValues);
...
// Original: return tempArray
return SortEnumValuesByIndex(tempArray);
}
And adding my function to Sort the enum by Index (Original code had problems when going across Projects ..), and Strip out any Values that don't have the [Description] attribute:
private object SortEnumValuesByIndex(Array enumValues)
{
var values = enumValues.Cast<Enum>().ToList();
var indexed = new Dictionary<int, Enum>();
foreach (var value in values)
{
int index = (int)Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()));
indexed.Add(index, value);
}
return indexed.OrderBy(x => x.Key).Select(x => x.Value).Where(x => x.HasDescriptionAttribute()).Cast<Enum>();
}
This example has been applied to ComboBoxes:
Note: Failures in Uploading images to the Server, so i added a URL to the images in question
<ComboBox x:Name="ConversionPreset_ComboBox" Grid.Row="4" Grid.Column="1" Margin="5,5,5,5" ItemsSource="{objects:EnumBindingSource EnumType={x:Type enums:ConversionPreset}}" SelectedIndex="2" SelectionChanged="ConversionPreset_ComboBox_SelectionChanged" />
<ComboBox x:Name="OutputType_ComboBox" Grid.Row="4" Grid.Column="2" Margin="5,5,5,5" ItemsSource="{objects:EnumBindingSource EnumType={x:Type enums:Output}}" SelectedIndex="1" SelectionChanged="OutputType_ComboBox_SelectionChanged" />
With code behind:
private Enumeration.Output Output { get; set; }
private void OutputType_ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
=> Output = (Enumeration.Output)OutputType_ComboBox.SelectedItem;
private Enumeration.ConversionPreset ConversionPreset { get; set; }
private void ConversionPreset_ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
=> ConversionPreset = (Enumeration.ConversionPreset)ConversionPreset_ComboBox.SelectedItem;
The "ConversionPreset" Enum and a Picture of the ComboBox Rendered
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
public enum ConversionPreset
{
[Description("Very Slow (Smaller File Size)")]
VerySlow = -2,
[Description("Slow (Smaller File Size)")]
Slow = -1,
[Description("Medium (Balanced File Size)")]
Medium = 0,
[Description("Fast (Bigger File Size)")]
Fast = 1,
[Description("Very Fast (Bigger File Size)")]
VeryFast = 2,
[Description("Ultra Fast (Biggest File Size)")]
UltraFast = 3
}
The "Output" Enum and a Picture of the ComboBox Rendered
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
public enum Output
{
// This will be hidden in the Output
None = -1,
[Description("Video")]
Video = 0,
[Description("Audio")]
Audio = 1
}
Update
The code above is intended to Bind Enums and Manage their Output without the need for Code Behind to manually fill the ComboBoxes.
I just now read that you want to approach it from in Code, having more control over the ComboBox Content.
This should get you on your way just fine. Just create a class and paste this content into it:
using System;
using System.ComponentModel;
namespace Attributes
{
public class Hidden : Attribute { }
public class StartingDescription : DescriptionAttribute
{
public StartingDescription(string description) : base(description) { }
}
public class FinishedDescription : DescriptionAttribute
{
public FinishedDescription(string description) : base(description) { }
}
}
namespace Enumerations
{
using Attributes;
public enum Order
{
None = -1,
[Description("Get"),
StartingDescription("Getting"),
FinishedDescription("Got")]
Get = 0,
[Description("Initialize"),
StartingDescription("Initializing"),
FinishedDescription("Initialized")]
Initialize = 1,
[Description("Download"),
StartingDescription("Downloading"),
FinishedDescription("Downloaded")]
Download = 2
}
public enum Output
{
[Hidden]
None = -1,
[Description("Video")]
Video = 33,
[Description("Audio")]
Audio = 44
}
}
namespace Classes
{
using System.Linq;
public static class Extensions
{
public static string Format(this string value, params object?[] args)
=> String.Format(value, args);
public static bool HasAttribute(this Enum enumValue, Type attributeType)
{
var result = enumValue.GetType().GetField(enumValue.ToString())
.GetCustomAttributes(attributeType, false)
.FirstOrDefault();
return (result != null);
}
public static int GetIndex(this Enum enumValue)
=> (int)Convert.ChangeType(enumValue, Enum.GetUnderlyingType(enumValue.GetType()));
public static string GetDescription(this Enum enumValue, Type attributetype = null)
{
if (attributetype == null)
attributetype = typeof(DescriptionAttribute);
var field = enumValue.GetType().GetField(enumValue.ToString());
var attributes = Attribute.GetCustomAttributes(field, false).Where(x => x.GetType().Equals(attributetype)).Cast<DescriptionAttribute>();
var value = attributes.FirstOrDefault();
if (value != null)
return value.Description;
return enumValue.ToString();
}
}
}
namespace ExecutionNamespace
{
using System.Collections.Generic;
using System.Linq;
using Classes;
using Attributes;
using System.Diagnostics;
internal class Example
{
public static SortedList<int, string> GetEnumByIndex(Type enumType, Type outputType = null)
{
if (enumType == null)
throw new ArgumentNullException("enumType was not Definied");
var enumValues = Enum.GetValues(enumType).Cast<Enum>().ToList();
if ((enumValues == null) || (enumValues.Count == 0))
throw new ArgumentNullException("Could not find any Enumeration Values");
if (outputType == null)
outputType = typeof(DescriptionAttribute);
var indexed = new SortedList<int, string>();
foreach (var value in enumValues)
if (!value.HasAttribute(typeof(Hidden)))
indexed.Add(value.GetIndex(), value.GetDescription(outputType));
return indexed;
}
public static SortedList<string, string> GetEnumByValue(Type enumType, Type outputType = null)
{
if (enumType == null)
throw new ArgumentNullException("enumType was not Definied");
var enumValues = Enum.GetValues(enumType).Cast<Enum>().ToList();
if ((enumValues == null) || (enumValues.Count == 0))
throw new ArgumentNullException("Could not find any Enumeration Values");
if (outputType == null)
outputType = typeof(DescriptionAttribute);
var indexed = new SortedList<string, string>();
foreach (var value in enumValues)
if (!value.HasAttribute(typeof(Hidden)))
indexed.Add(value.ToString(), value.GetDescription(outputType));
return indexed;
}
public static void Run()
{
Type type = null;
type = typeof(Enumerations.Order);
Debug.WriteLine("{0} by Index".Format(type.ToString()));
foreach (var valuePair in GetEnumByIndex(type, typeof(StartingDescription)))
Debug.WriteLine("Index:{1} & Description:{2} ".Format(type.ToString(), valuePair.Key, valuePair.Value));
Debug.WriteLine("");
type = typeof(Enumerations.Order);
Debug.WriteLine("{0} by Value".Format(type.ToString()));
foreach (var valuePair in GetEnumByValue(type, typeof(StartingDescription)))
Debug.WriteLine("Value:{1} & Description:{2} ".Format(type.ToString(), valuePair.Key, valuePair.Value));
Debug.WriteLine("");
type = typeof(Enumerations.Output);
Debug.WriteLine("{0} by Index".Format(type.ToString()));
foreach (var valuePair in GetEnumByIndex(type))
Debug.WriteLine("Index:{1} & Description:{2} ".Format(type.ToString(), valuePair.Key, valuePair.Value));
Debug.WriteLine("");
type = typeof(Enumerations.Output);
Debug.WriteLine("{0} by Value".Format(type.ToString()));
foreach (var valuePair in GetEnumByValue(type))
Debug.WriteLine("Value:{1} & Description:{2} ".Format(type.ToString(), valuePair.Key, valuePair.Value));
Debug.WriteLine("");
}
}
}
Then just do this on like a Button Click, and watch the Output Window:
private void Button_Click(object sender, RoutedEventArgs e)
{
ExecutionNamespace.Example.Run();
}
It will bring you this output:
Enumerations.Order by Index
Index:-1 & Description:None
Index:0 & Description:Getting
Index:1 & Description:Initializing
Index:2 & Description:Downloading
Enumerations.Order by Value
Value:Download & Description:Downloading
Value:Get & Description:Getting
Value:Initialize & Description:Initializing
Value:None & Description:None
Enumerations.Output by Index
Index:33 & Description:Video
Index:44 & Description:Audio
Enumerations.Output by Value
Value:Audio & Description:Audio
Value:Video & Description:Video

Related

WPF Validation with Data Annotations

Hi am struggling with validation with data annotations in WPF
I have a class in a separate project because we are reusing the class across multiple projects.Several of the fields have data annotations for validation.
the validation class combines the errors returned from data annotations with some custom business rules which are different per client
internal class Validation<T> : ValidationRule where T : class, ICore
{
IUnitofWork unitofWork;
List<Func<T, bool>> CompiledRules = new List<Func<T, bool>>();
List<Rule<T>> Rules = new List<Rule<T>>();
Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
List<Func<Object, bool>> FieldRules = new List<Func<object, bool>>();
public Validation()
{
unitofWork = new UnitOfWork();
CompileRules();
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
return IsValid((T)value);
}
public ValidationResult Validate(object value,string PropertyName ,CultureInfo cultureInfo)
{
return IsValid((T)value,PropertyName);
}
void CompileRules()
{
Type t = typeof(T);
List<BusinessRule> rules = unitofWork.Repository<BusinessRule>().GetAll(b => b.Name == t.Name && b.Enabled == true);
foreach (BusinessRule b in rules)
{
Func<T, bool> CompiledRule = CompileRule(b);
CompiledRules.Add(CompiledRule);
Rule<T> rule = new Rule<T>();
rule.CompiledRule = CompiledRule;
rule.ErrorMessage = b.MessageTemplate;
rule.FieldName = b.Field;
Rules.Add(rule);
}
}
ValidationResult IsValid(T Value)
{
bool valid = true;
_errors.Clear();
if (CompiledRules.Count > 0 || Value.Errors.Count > 0)
{
//valid = CompiledRules.All(rule => rule(Value));
foreach (Rule<T> r in Rules)
{
bool isValid = r.CompiledRule(Value);
r.PassedRule = isValid;
string field = r.FieldName;
if (!isValid )
{
valid = false;
string ErrorMessage = string.Format("{0} {1}", r.FieldName, r.ErrorMessage);
if (_errors.ContainsKey(field))
_errors[field].Add(ErrorMessage);
else
{
List<string> el = new List<string>();
el.Add(ErrorMessage);
_errors.Add(field, el);
}
}
}
}
ValidateAnnotations(Value,ref valid);
return new ValidationResult(valid, _errors);
}
void ValidateAnnotations(object Value,ref bool Valid)
{
DataAnnotationValidator annotationValidator = new DataAnnotationValidator();
List< System.ComponentModel.DataAnnotations.ValidationResult> results = annotationValidator.ValidateObject(Value);
if (results.Count > 0)
{
Valid = false;
foreach (System.ComponentModel.DataAnnotations.ValidationResult r in results)
{
if (_errors.ContainsKey(r.MemberNames.First()))
{
_errors[r.MemberNames.First()].Add(r.ErrorMessage);
}
else
{
List<string> propErrors = new List<string>();
propErrors.Add(r.ErrorMessage);
_errors.Add(r.MemberNames.First(), propErrors);
}
}
}
}
void ValidateAnnotations(object Value,string PropertyName, ref bool Valid)
{
DataAnnotationValidator annotationValidator = new DataAnnotationValidator();
List<System.ComponentModel.DataAnnotations.ValidationResult> results = annotationValidator.ValidateObject(Value, PropertyName);
if (results.Count > 0)
{
Valid = false;
foreach (System.ComponentModel.DataAnnotations.ValidationResult r in results)
{
if (_errors.ContainsKey(r.MemberNames.First()))
{
_errors[r.MemberNames.First()].Add(r.ErrorMessage);
}
else
{
List<string> propErrors = new List<string>();
propErrors.Add(r.ErrorMessage);
_errors.Add(r.MemberNames.First(), propErrors);
}
}
}
}
ValidationResult IsValid(T Value, string PropertyName)
{
_errors.Remove(PropertyName);
bool valid = true;
if (CompiledRules.Count > 0)
{
//valid = CompiledRules.All(rule => rule(Value));
foreach (Rule<T> r in Rules.Where(b=>b.FieldName == PropertyName))
{
bool isValid = r.CompiledRule(Value);
r.PassedRule = isValid;
string field = r.FieldName;
// string field = "SelectedRow." + r.FieldName;
if (!isValid)
{
valid = false;
string ErrorMessage = string.Format("{0} {1}", r.FieldName, r.ErrorMessage);
if (_errors.ContainsKey(field))
_errors[field].Add(ErrorMessage);
else
{
List<string> el = new List<string>();
el.Add(ErrorMessage);
_errors.Add(field, el);
}
}
}
}
ValidateAnnotations(Value,PropertyName, ref valid);
return new ValidationResult(valid, _errors);
}
public Func<T, bool> CompileRule(BusinessRule r)
{
var paramT = Expression.Parameter(typeof(T));
Expression expression = BuildExpr(r, paramT);
return Expression.Lambda<Func<T, bool>>(expression, paramT).Compile();
}
static Expression BuildExpr(BusinessRule r, ParameterExpression param)
{
var left = MemberExpression.Property(param, r.Field);
var tProp = typeof(T).GetProperty(r.Field).PropertyType;
ExpressionType tBinary;
// is the operator a known .NET operator?
if (ExpressionType.TryParse(r.Operator, out tBinary))
{
var right = Expression.Constant(Convert.ChangeType(r.CompareValue, tProp));
// use a binary operation, e.g. 'Equal' -> 'u.Age == 15'
return Expression.MakeBinary(tBinary, left, right);
}
else
{
var method = tProp.GetMethod(r.Operator);
var tParam = method.GetParameters()[0].ParameterType;
var right = Expression.Constant(Convert.ChangeType(r.CompareValue, tParam));
// use a method call, e.g. 'Contains' -> 'u.Tags.Contains(some_tag)'
return Expression.Call(left, method, right);
}
}
}
internal class Rule<T> where T : class, ICore
{
public string FieldName
{ get;set; }
public Func<T, bool> CompiledRule
{ get; set; }
public Func<object,bool> RevisedRule
{ get; set; }
public string ErrorMessage
{ get; set; }
public bool PassedRule
{ get; set; }
}
internal class Error
{
public string ErrorMessage { get; set; }
public string FieldName { get; set; }
}
The is working as the _errors list has the field names and any errors associated with them.
once we have these we loop through and raise the onPropertyErrorsChangedEvent
internal void SetErrorDetails(ValidationResult Result)
{
propErrors = (Dictionary<string, List<string>>)Result.ErrorContent;
foreach (string key in propErrors.Keys)
{
OnPropertyErrorsChanged(key);
}
}
on my view the 2 fields are
<TextBox Canvas.Left="138"
Canvas.Top="75"
FontFamily="Verdana"
HorizontalAlignment="Left"
Height="20"
Text="{Binding OrganizationName,ValidatesOnDataErrors=True,NotifyOnValidationError=True,ValidatesOnNotifyDataErrors=True,ValidatesOnExceptions=True}" VerticalAlignment="Top" Width="137"
Validation.ErrorTemplate="{StaticResource ValidationTemplate }"
Style="{StaticResource TextErrorStyle}"/>
<TextBox Canvas.Left="138"
Canvas.Top="225"
FontFamily="Verdana"
HorizontalAlignment="Left"
Height="20"
Text="{Binding SelectedRow.Postcode,ValidatesOnDataErrors=True,ValidatesOnNotifyDataErrors=True,ValidatesOnExceptions=True}"
VerticalAlignment="Top"
Width="137"
Validation.ErrorTemplate="{StaticResource ValidationTemplate }"
Style="{StaticResource TextErrorStyle}" />
I am encountering 2 problems i am encountering
When bound directly to the selectedrow (Postcode) my data annotations appear when the form is loaded but when bound via a field on my view model (Organisation Name) they do not . We need to bind these to fields on the view model so that the business rules get run as part of validation.
Second problem if i save the form with an invalid entry for the organisation the save stops because it is invalid however i don't get an error notification even though there is an error for the property in the _errors.
I am not sure what i am doing wrong could someone point me in the right direction please?
[Edit]
We use a third party document service to create and show the view
void CreateDocument(object Arg)
{
string title = string.Empty;
if (Arg.ToString().ToLower() == "edit" && SelectedRow !=null)
{
if (SelectedRow.OrganizationName != null)
title = SelectedRow.OrganizationName;
}
else
{
SelectedRow = new Address();
title = "New Address";
}
AddressDetailVM detail = new AddressDetailVM(SelectedRow,this);
Document = iInternal.CreateDocument("AddressDetails",
detail,
title
);
detail.Document = Document;
// Document = iInternal.CreateDocument("AddressDetails", null, this, title);
Document.Show();
}

Combined constants in WPF XAML

I have two constants defined in XAML and I would like to define the third based on that two:
<UserControl ... xmlns:clr="clr-namespace:System;assembly=mscorlib">
<UserControl.Resources>
<clr:Int32 x:Key="Constant1">1</clr:Int32>
<clr:Int32 x:Key="Constant2">2</clr:Int32>
<!-- Is it possible to achieve something like this? -->
<clr:Int32 x:Key="Constant3">{StaticResource Constant1} + {StaticResource Constant2}</clr:Int32>
</UserControl.Resources>
</UserControl>
Is it possible?
It is possible, but not as you want to do it.
One solution I can think of is a custom MarkupExtension like this:
[MarkupExtensionReturnType(typeof(int))]
public class IntCalculator:MarkupExtension
{
public List<int> Values { get; set; }
public List<string> ResourceNames { get; set; }
public IntCalculator()
{
Values = new List<int>();
ResourceNames = new List<string>();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
var root = (IRootObjectProvider)serviceProvider.GetService(typeof(IRootObjectProvider));
var rootObject = root.RootObject as FrameworkElement;
int calcVal = 0;
if (rootObject != null)
{
foreach (var resourceName in ResourceNames)
{
var resource = rootObject.FindResource(resourceName);
if (resource != null && resource is int)
{
calcVal += System.Convert.ToInt32(resource);
}
}
}
foreach (var value in Values)
{
calcVal += value;
}
return calcVal;
}
}
With this Extension you can add int resources or int values.
This is how to use it:
in your resources:
<local:IntCalculator x:Key="CalcVal">
<local:IntCalculator.ResourceNames>
<clr:String>Constant1</clr:String>
<clr:String>Constant2</clr:String>
</local:IntCalculator.ResourceNames>
</local:IntCalculator>
to display the value:
<Label Content="{StaticResource CalcVal}"/>

XDocument binding Elements and Attributes

I have an XDocument like this one set as a DataContext of my Window:
Class MainWindow
Public Sub New()
InitializeComponent()
Me.DataContext = <?xml version="1.0" encoding="utf-8"?>
<Sketch Format="A4" Author="Aaron" Created="..." Test="Value">
<Item Kind="Line" X1="50" Y1="50" X2="150" Y2="150">
<Item Kind="Rect" X="10" Y="10" Width="30" Height="30"/>
</Item>
<Item Kind="Line" X1="250" Y1="250" X2="250" Y2="50">
<Item Kind="Ellipse" X="10" Y="10" Width="30" Height="30"/>
</Item>
<Test Param="Value"/>
</Sketch>
End Sub
End Class
Now in my frontend I test couple of different binding paths. All of them works with Elements, Element, Attribute, but Attributes doesn't seem to work for me. I consider it rather odd, because Elements is IEnumerable<XElement> and Attributes is IEnumerable<XAttribute> -- exactly the same kind of collection and everything.
<Window Height="320" Title="Main Window" Width="640" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="MainWindow">
<UniformGrid Columns="3">
<StackPanel>
<Label Foreground="DimGray">Root.Elements.Count</Label>
<Label Content="{Binding Path=Root.Elements.Count, FallbackValue=Loading…}"/>
<Label Foreground="DimGray">Root.Attributes.Count</Label>
<Label Content="{Binding Path=Root.Attributes.Count, FallbackValue=Loading…}"/>
<Label Foreground="DimGray">Root.Element[Test]</Label>
<Label Content="{Binding Path=Root.Element[Test], FallbackValue=Loading…}"/>
<Label Foreground="DimGray">Root.Attribute[Test]</Label>
<Label Content="{Binding Path=Root.Attribute[Test], FallbackValue=Loading…}"/>
</StackPanel>
<StackPanel>
<Label Foreground="DimGray">Root.Elements</Label>
<ListBox ItemsSource="{Binding Root.Elements}"/>
<Label Foreground="DimGray">Root.Attributes</Label>
<ListBox ItemsSource="{Binding Root.Attributes}"/>
</StackPanel>
<StackPanel>
<TreeView ItemsSource="{Binding Root.Elements}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Elements}">
<Label Content="{Binding Name}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</StackPanel>
</UniformGrid>
</Window>
Do you have any idea why everything binds correctly except Attributes? Any help is appreciated. I think is has (maybe) got something to do with a fact, that Element and Elements are inherited from XContainer, but this doesn't explain why XElements very own Attribute works...
Thanks in advance!
Aaron
There is no property Attributes on XElement (only method Attributes() that can't be used directly in binding), so it's not surprising the binding doesn't work.
But there is also no property Elements, so why does that work? It's because LINQ to XML objects have special “dynamic properties” specifically for use in WPF, see LINQ to XML Dynamic Properties. There is a dynamic property Elements on XElement, but no Attributes.
There's still one thing I don't understand though: The Elements dynamic property is documented to work only in the form elem.Elements[elementName]. So it's still surprising to me that your code works.
If you want to know about any workarounds, I can't think of any, except for invoking the Attributes() method using <ObjectDataProvider>.
Svick is correct with his response. The reason why Elements works as you have discovered is that the custom CustomTypeDescriptor for XElement ( determined by the presence of the TypeDescriptionProviderAttribute on XElement ) provides a custom PropertyDescriptor with name Elements which returns an IEnumerable<XElement>. If this is followed in the binding path by an indexer then what is returned is XContainer.Elements(XName) otherwise will be XContainer.Elements(). The reason why Attributes does not work is that there is no such dynamic property descriptor provided.
The code below provides this missing functionality ( and also a Nodes property ) in a similar manner to the dynamic Elements property.
//Add this code in App start up
TypeDescriptor.AddProvider(new XElementAdditionalDynamicPropertiesTypeDescriptionProvider(),
typeof(XElement));
The classes below provide the functionality and this code is similar to how Elements work.
public class XDeferredAxis : IEnumerable<XAttribute>
{
internal XElement element;
private Func<XElement, XName, IEnumerable<XAttribute>> func;
private XName name;
public IEnumerator<XAttribute> GetEnumerator()
{
return this.func(this.element, this.name).GetEnumerator();
}
public XDeferredAxis(Func<XElement, XName, IEnumerable<XAttribute>> func, XElement element, XName name)
{
if (func == null)
{
throw new ArgumentNullException("func");
}
if (element == null)
{
throw new ArgumentNullException("element");
}
this.func = func;
this.element = element;
this.name = name;
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
public class XElementNodesPropertyDescriptor : PropertyDescriptor
{
private XElement element;
private bool childRemoved;
public XElementNodesPropertyDescriptor() : base("Nodes", null)
{
}
public override void AddValueChanged(object component, EventHandler handler)
{
bool flag = base.GetValueChangedHandler(component) != null;
base.AddValueChanged(component, handler);
if (!flag)
{
XElement local = component as XElement;
if ((local != null) && (base.GetValueChangedHandler(component) != null))
{
element = local;
local.Changing += new EventHandler<XObjectChangeEventArgs>(this.OnChanging);
local.Changed += new EventHandler<XObjectChangeEventArgs>(this.OnChanged);
}
}
}
private void OnChanging(object sender, XObjectChangeEventArgs e)
{
childRemoved = false;
if (e.ObjectChange == XObjectChange.Remove)
{
XObject senderNode = (XObject)sender;
if (senderNode.Parent == element)
{
childRemoved = true;
}
}
}
private void OnChanged(object sender, XObjectChangeEventArgs e)
{
XObject senderNode = (XObject)sender;
switch (e.ObjectChange)
{
case XObjectChange.Add:
case XObjectChange.Value:
case XObjectChange.Name:
if (senderNode.Parent == element)
{
this.OnValueChanged(element, EventArgs.Empty);
}
break;
case XObjectChange.Remove:
if (childRemoved)
{
this.OnValueChanged(element, EventArgs.Empty);
}
break;
}
}
public override void RemoveValueChanged(object component, EventHandler handler)
{
base.RemoveValueChanged(component, handler);
XElement local = component as XElement;
if ((local != null) && (base.GetValueChangedHandler(component) == null))
{
local.Changed -= new EventHandler<XObjectChangeEventArgs>(this.OnChanged);
}
}
public override bool SupportsChangeEvents
{
get
{
return true;
}
}
public override Type ComponentType
{
get
{
return typeof(XElement);
}
}
public override bool IsReadOnly
{
get
{
return true;
}
}
public override Type PropertyType
{
get
{
return typeof(IEnumerable<XNode>);
}
}
public override bool CanResetValue(object component)
{
return false;
}
public override object GetValue(object component)
{
var nodes= (component as XElement).Nodes();
return nodes;
}
public override void ResetValue(object component)
{
}
public override void SetValue(object component, object value)
{
}
public override bool ShouldSerializeValue(object component)
{
return false;
}
}
public class XElementAttributesPropertyDescriptor : PropertyDescriptor
{
private XDeferredAxis value;
private bool removalIsOwnAttribute;
public XElementAttributesPropertyDescriptor() : base("Attributes", null) {
}
public override void AddValueChanged(object component, EventHandler handler)
{
bool flag = base.GetValueChangedHandler(component) != null;
base.AddValueChanged(component, handler);
if (!flag)
{
XElement local = component as XElement;
if ((local != null) && (base.GetValueChangedHandler(component) != null))
{
local.Changing += new EventHandler<XObjectChangeEventArgs>(this.OnChanging);
local.Changed += new EventHandler<XObjectChangeEventArgs>(this.OnChanged);
}
}
}
private void OnChanging(object sender, XObjectChangeEventArgs e)
{
removalIsOwnAttribute = false;
if (e.ObjectChange == XObjectChange.Remove)
{
var xAttribute = sender as XAttribute;
if (xAttribute != null && xAttribute.Parent == value.element)
{
removalIsOwnAttribute = true;
}
}
}
private void OnChanged(object sender, XObjectChangeEventArgs e)
{
var changeRequired = false;
var xAttribute = sender as XAttribute;
if (xAttribute != null)
{
switch (e.ObjectChange)
{
case XObjectChange.Name:
case XObjectChange.Add:
if (xAttribute.Parent == value.element)
{
changeRequired = true;
}
break;
case XObjectChange.Remove:
changeRequired = removalIsOwnAttribute;
break;
}
if (changeRequired)
{
this.OnValueChanged(value.element, EventArgs.Empty);
}
}
}
public override void RemoveValueChanged(object component, EventHandler handler)
{
base.RemoveValueChanged(component, handler);
XElement local = component as XElement;
if ((local != null) && (base.GetValueChangedHandler(component) == null))
{
local.Changed -= new EventHandler<XObjectChangeEventArgs>(this.OnChanged);
}
}
public override bool SupportsChangeEvents
{
get
{
return true;
}
}
public override Type ComponentType
{
get
{
return typeof(XElement);
}
}
public override bool IsReadOnly
{
get
{
return true;
}
}
public override Type PropertyType
{
get
{
return typeof(IEnumerable<XAttribute>);
}
}
public override bool CanResetValue(object component)
{
return false;
}
public override object GetValue(object component)
{
return (object)(this.value = new XDeferredAxis((Func<XElement, XName, IEnumerable<XAttribute>>)((e, n) =>
{
if (!(n != (XName)null))
return e.Attributes();
return e.Attributes(n);
}), component as XElement, (XName)null));
}
public override void ResetValue(object component)
{
}
public override void SetValue(object component, object value)
{
}
public override bool ShouldSerializeValue(object component)
{
return false;
}
}
public class XElementAdditionalDynamicPropertiesTypeDescriptionProvider: TypeDescriptionProvider
{
public XElementAdditionalDynamicPropertiesTypeDescriptionProvider() : this(TypeDescriptor.GetProvider(typeof(XElement))) { }
protected XElementAdditionalDynamicPropertiesTypeDescriptionProvider(TypeDescriptionProvider parent) : base(parent) { }
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
{
var baseTypeDescriptor= base.GetTypeDescriptor(objectType, instance);
return new XElementAdditionalDynamicPropertiesTypeDescriptor(baseTypeDescriptor);
}
}
public class XElementAdditionalDynamicPropertiesTypeDescriptor : CustomTypeDescriptor
{
public XElementAdditionalDynamicPropertiesTypeDescriptor(ICustomTypeDescriptor original) : base(original) { }
public override PropertyDescriptorCollection GetProperties()
{
return GetProperties(null);
}
public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
PropertyDescriptorCollection descriptors = new PropertyDescriptorCollection(null);
if (attributes == null)
{
descriptors.Add(new XElementAttributesPropertyDescriptor());
descriptors.Add(new XElementNodesPropertyDescriptor());
}
foreach (PropertyDescriptor pd in base.GetProperties(attributes))
{
descriptors.Add(pd);
}
return descriptors;
}
}
A quick portable workaround for this issue is to run the XElement through a converter that returns its Attributes results. Then you can simply bind to the element.
I also filter out namespaces below, easily removable.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Xml.Linq;
namespace FSW.Core.Utility
{
[ValueConversion(typeof(XElement), typeof(IEnumerable<XAttribute>))]
public class XElementToXAttributesConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var element = value as XElement;
return element?.Attributes().Where(x=>x.Name.LocalName != "xmlns");
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

How to bind a TextBlock to a resource containing formatted text?

I have a TextBlock in my WPF window.
<TextBlock>
Some <Bold>formatted</Bold> text.
</TextBlock>
When it is rendered it looks like this,
Some formatted text.
My question is, can I bind this inline "content" to a resource in my application?
I got as far as:
Making an application resource string,
myText="Some <Bold>formatted</Bold> text."
and the following xaml (Some code omitted for brevity)
<Window xmlns:props="clr-namespace:MyApp.Properties">
<Window.Resources>
<props:Resources x:Key="Resources"/>
</Window.Resources>
<TextBlock x:Name="Try1"
Text="{Binding Source={StaticResource Resources} Path=myText}"/>
<TextBlock x:Name="Try2">
<Binding Source="{StaticResource Resources}" Path="myText" />
</TextBlock>
</Window>
Try1 renders with the tags in place and not effecting formatting.
Some <Bold>formatted<Bold> text.
Try2 will not compile or render because the resource "myText" is not of type Inline but a string.
Is this seemingly simple task possible and if so how?
Here is my modified code for recursively format text. It handles Bold, Italic, Underline and LineBreak but can easily be extended to support more (modify the switch statement).
public static class MyBehavior
{
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(MyBehavior),
new UIPropertyMetadata("", FormattedTextChanged));
static Inline Traverse(string value)
{
// Get the sections/inlines
string[] sections = SplitIntoSections(value);
// Check for grouping
if (sections.Length.Equals(1))
{
string section = sections[0];
string token; // E.g <Bold>
int tokenStart, tokenEnd; // Where the token/section starts and ends.
// Check for token
if (GetTokenInfo(section, out token, out tokenStart, out tokenEnd))
{
// Get the content to further examination
string content = token.Length.Equals(tokenEnd - tokenStart) ?
null :
section.Substring(token.Length, section.Length - 1 - token.Length * 2);
switch (token)
{
case "<Bold>":
return new Bold(Traverse(content));
case "<Italic>":
return new Italic(Traverse(content));
case "<Underline>":
return new Underline(Traverse(content));
case "<LineBreak/>":
return new LineBreak();
default:
return new Run(section);
}
}
else return new Run(section);
}
else // Group together
{
Span span = new Span();
foreach (string section in sections)
span.Inlines.Add(Traverse(section));
return span;
}
}
/// <summary>
/// Examines the passed string and find the first token, where it begins and where it ends.
/// </summary>
/// <param name="value">The string to examine.</param>
/// <param name="token">The found token.</param>
/// <param name="startIndex">Where the token begins.</param>
/// <param name="endIndex">Where the end-token ends.</param>
/// <returns>True if a token was found.</returns>
static bool GetTokenInfo(string value, out string token, out int startIndex, out int endIndex)
{
token = null;
endIndex = -1;
startIndex = value.IndexOf("<");
int startTokenEndIndex = value.IndexOf(">");
// No token here
if (startIndex < 0)
return false;
// No token here
if (startTokenEndIndex < 0)
return false;
token = value.Substring(startIndex, startTokenEndIndex - startIndex + 1);
// Check for closed token. E.g. <LineBreak/>
if (token.EndsWith("/>"))
{
endIndex = startIndex + token.Length;
return true;
}
string endToken = token.Insert(1, "/");
// Detect nesting;
int nesting = 0;
int temp_startTokenIndex = -1;
int temp_endTokenIndex = -1;
int pos = 0;
do
{
temp_startTokenIndex = value.IndexOf(token, pos);
temp_endTokenIndex = value.IndexOf(endToken, pos);
if (temp_startTokenIndex >= 0 && temp_startTokenIndex < temp_endTokenIndex)
{
nesting++;
pos = temp_startTokenIndex + token.Length;
}
else if (temp_endTokenIndex >= 0 && nesting > 0)
{
nesting--;
pos = temp_endTokenIndex + endToken.Length;
}
else // Invalid tokenized string
return false;
} while (nesting > 0);
endIndex = pos;
return true;
}
/// <summary>
/// Splits the string into sections of tokens and regular text.
/// </summary>
/// <param name="value">The string to split.</param>
/// <returns>An array with the sections.</returns>
static string[] SplitIntoSections(string value)
{
List<string> sections = new List<string>();
while (!string.IsNullOrEmpty(value))
{
string token;
int tokenStartIndex, tokenEndIndex;
// Check if this is a token section
if (GetTokenInfo(value, out token, out tokenStartIndex, out tokenEndIndex))
{
// Add pretext if the token isn't from the start
if (tokenStartIndex > 0)
sections.Add(value.Substring(0, tokenStartIndex));
sections.Add(value.Substring(tokenStartIndex, tokenEndIndex - tokenStartIndex));
value = value.Substring(tokenEndIndex); // Trim away
}
else
{ // No tokens, just add the text
sections.Add(value);
value = null;
}
}
return sections.ToArray();
}
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
string value = e.NewValue as string;
TextBlock textBlock = sender as TextBlock;
if (textBlock != null)
textBlock.Inlines.Add(Traverse(value));
}
}
Edit: (proposed by Spook)
A shorter version, but requires the text to be XML-valid:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Xml;
// (...)
public static class TextBlockHelper
{
#region FormattedText Attached dependency property
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(TextBlockHelper),
new UIPropertyMetadata("", FormattedTextChanged));
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
string value = e.NewValue as string;
TextBlock textBlock = sender as TextBlock;
if (textBlock != null)
{
textBlock.Inlines.Clear();
textBlock.Inlines.Add(Process(value));
}
}
#endregion
static Inline Process(string value)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(value);
Span span = new Span();
InternalProcess(span, doc.ChildNodes[0]);
return span;
}
private static void InternalProcess(Span span, XmlNode xmlNode)
{
foreach (XmlNode child in xmlNode)
{
if (child is XmlText)
{
span.Inlines.Add(new Run(child.InnerText));
}
else if (child is XmlElement)
{
Span spanItem = new Span();
InternalProcess(spanItem, child);
switch (child.Name.ToUpper())
{
case "B":
case "BOLD":
Bold bold = new Bold(spanItem);
span.Inlines.Add(bold);
break;
case "I":
case "ITALIC":
Italic italic = new Italic(spanItem);
span.Inlines.Add(italic);
break;
case "U":
case "UNDERLINE":
Underline underline = new Underline(spanItem);
span.Inlines.Add(underline);
break;
}
}
}
}
}
And an example of usage:
<RootItem xmlns:u="clr-namespace:MyApp.Helpers">
<TextBlock u:TextBlockHelper.FormattedText="{Binding SomeProperty}" />
</RootItem>
I've added hyperlink and image support to Vincents solution:
public static class FormattedTextBlock
{
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(FormattedTextBlock),
new UIPropertyMetadata("", FormattedTextChanged));
static Inline Traverse(string value)
{
// Get the sections/inlines
string[] sections = SplitIntoSections(value);
// Check for grouping
if(sections.Length.Equals(1))
{
string section = sections[0];
string token; // E.g <Bold>
int tokenStart, tokenEnd; // Where the token/section starts and ends.
// Check for token
if(GetTokenInfo(section, out token, out tokenStart, out tokenEnd))
{
// Get the content to further examination
string content = token.Length.Equals(tokenEnd - tokenStart) ?
null :
section.Substring(token.Length, section.Length - 1 - token.Length * 2);
switch(token.ToUpper())
{
case "<B>":
case "<BOLD>":
/* <b>Bold text</b> */
return new Bold(Traverse(content));
case "<I>":
case "<ITALIC>":
/* <i>Italic text</i> */
return new Italic(Traverse(content));
case "<U>":
case "<UNDERLINE>":
/* <u>Underlined text</u> */
return new Underline(Traverse(content));
case "<BR>":
case "<BR/>":
case "<LINEBREAK/>":
/* Line 1<br/>line 2 */
return new LineBreak();
case "<A>":
case "<LINK>":
/* <a>{http://www.google.de}Google</a> */
var start = content.IndexOf("{");
var end = content.IndexOf("}");
var url = content.Substring(start + 1, end - 1);
var text = content.Substring(end + 1);
var link = new Hyperlink();
link.NavigateUri = new System.Uri(url);
link.RequestNavigate += Hyperlink_RequestNavigate;
link.Inlines.Add(text);
return link;
case "<IMG>":
case "<IMAGE>":
/* <image>pack://application:,,,/ProjectName;component/directory1/directory2/image.png</image> */
var image = new Image();
var bitmap = new BitmapImage(new Uri(content));
image.Source = bitmap;
image.Width = bitmap.Width;
image.Height = bitmap.Height;
var container = new InlineUIContainer();
container.Child = image;
return container;
default:
return new Run(section);
}
}
else return new Run(section);
}
else // Group together
{
Span span = new Span();
foreach(string section in sections)
span.Inlines.Add(Traverse(section));
return span;
}
}
static void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
Process.Start(e.Uri.ToString());
}
/// <summary>
/// Examines the passed string and find the first token, where it begins and where it ends.
/// </summary>
/// <param name="value">The string to examine.</param>
/// <param name="token">The found token.</param>
/// <param name="startIndex">Where the token begins.</param>
/// <param name="endIndex">Where the end-token ends.</param>
/// <returns>True if a token was found.</returns>
static bool GetTokenInfo(string value, out string token, out int startIndex, out int endIndex)
{
token = null;
endIndex = -1;
startIndex = value.IndexOf("<");
int startTokenEndIndex = value.IndexOf(">");
// No token here
if(startIndex < 0)
return false;
// No token here
if(startTokenEndIndex < 0)
return false;
token = value.Substring(startIndex, startTokenEndIndex - startIndex + 1);
// Check for closed token. E.g. <LineBreak/>
if(token.EndsWith("/>"))
{
endIndex = startIndex + token.Length;
return true;
}
string endToken = token.Insert(1, "/");
// Detect nesting;
int nesting = 0;
int temp_startTokenIndex = -1;
int temp_endTokenIndex = -1;
int pos = 0;
do
{
temp_startTokenIndex = value.IndexOf(token, pos);
temp_endTokenIndex = value.IndexOf(endToken, pos);
if(temp_startTokenIndex >= 0 && temp_startTokenIndex < temp_endTokenIndex)
{
nesting++;
pos = temp_startTokenIndex + token.Length;
}
else if(temp_endTokenIndex >= 0 && nesting > 0)
{
nesting--;
pos = temp_endTokenIndex + endToken.Length;
}
else // Invalid tokenized string
return false;
} while(nesting > 0);
endIndex = pos;
return true;
}
/// <summary>
/// Splits the string into sections of tokens and regular text.
/// </summary>
/// <param name="value">The string to split.</param>
/// <returns>An array with the sections.</returns>
static string[] SplitIntoSections(string value)
{
List<string> sections = new List<string>();
while(!string.IsNullOrEmpty(value))
{
string token;
int tokenStartIndex, tokenEndIndex;
// Check if this is a token section
if(GetTokenInfo(value, out token, out tokenStartIndex, out tokenEndIndex))
{
// Add pretext if the token isn't from the start
if(tokenStartIndex > 0)
sections.Add(value.Substring(0, tokenStartIndex));
sections.Add(value.Substring(tokenStartIndex, tokenEndIndex - tokenStartIndex));
value = value.Substring(tokenEndIndex); // Trim away
}
else
{ // No tokens, just add the text
sections.Add(value);
value = null;
}
}
return sections.ToArray();
}
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
string value = e.NewValue as string;
TextBlock textBlock = sender as TextBlock;
if(textBlock != null)
textBlock.Inlines.Add(Traverse(value));
}
}
Thanks Vincent for the great template, it works like a charm!
How about using attached behavior? Below code only handles bold tags. Each word which should be bold needs to be wrapped in bold tags. You probably want to make the class accept other formats as well. Also spaces needs to be handled better, the class strips out consecutive spaces and add one extra to the end. So consider below class as demo code only which will need further work to be useful but it should get you started.
XAML:
<Window x:Class="FormatTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:FormatTest="clr-namespace:FormatTest"
Title="Window1" Height="300" Width="300">
<TextBlock FormatTest:FormattedTextBehavior.FormattedText="{Binding Path=Text}" />
</Window>
Code behind:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
namespace FormatTest
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = this;
}
public string Text { get { return "Some <Bold>formatted</Bold> text."; } }
}
public static class FormattedTextBehavior
{
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(FormattedTextBehavior),
new UIPropertyMetadata("", FormattedTextChanged));
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
TextBlock textBlock = sender as TextBlock;
string value = e.NewValue as string;
string[] tokens = value.Split(' ');
foreach (string token in tokens)
{
if (token.StartsWith("<Bold>") && token.EndsWith("</Bold>"))
{
textBlock.Inlines.Add(new Bold(new Run(token.Replace("<Bold>", "").Replace("</Bold>", "") + " ")));
}
else
{
textBlock.Inlines.Add(new Run(token + " "));
}
}
}
}
}
EDIT:
This line,
<props:Resources x:Key="Resources"/>
is a bad approach to accesing the Project.Properties.Resources namespace. It causes awkward glitches when recompiling.
Much better to use x:Static to do somthing like this,
Text="{x:Static props:Resources.SomeText}"
in your binding. Thx to Ben
Okay, this is how I did it. It's not perfect but it works.
Remember, there is a project resource called FormattedText.
cs:
// TextBlock with a bindable InlineCollection property.
// Type is List(Inline) not InlineCollection becuase
// InlineCollection makes the IDE xaml parser complain
// presumably this is caused by an inherited attribute.
public class BindableTextBlock : TextBlock
{
public static readonly DependencyProperty InlineCollectionProperty =
DependencyProperty.Register(
"InlineCollection",
typeof(List<Inline>),
typeof(BindableTextBlock),
new UIPropertyMetadata(OnInlineCollectionChanged));
private static void OnInlineCollectionChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
BinableTextBlock instance = sender as BindableTextBlock;
if (instance != null)
{
List<Inline> newText = e.NewValue as List<Inline>;
if (newText != null)
{
// Clear the underlying Inlines property
instance.Inlines.Clear();
// Add the passed List<Inline> to the real Inlines
instance.Inlines.AddRange(newText.ToList());
}
}
}
public List<Inline> InlineCollection
{
get
{
return (List<Inline>)GetValue(InlineCollectionProperty);
}
set
{
SetValue(InlineCollectionProperty, value);
}
}
}
// Convertor between a string of xaml with implied run elements
// and a generic list of inlines
[ValueConversion(typeof(string), typeof(List<Inline>))]
public class StringInlineCollectionConvertor : IValueConverter
{
public object Convert(object value,
Type targetType,
object parameter,
System.Globalization.CultureInfo culture)
{
string text = value as String;
// a surrogate TextBlock to host an InlineCollection
TextBlock results = new TextBlock();
if (!String.IsNullOrEmpty(text))
{
//Arbritary literal acting as a replace token,
//must not exist in the empty xaml definition.
const string Replace = "xxx";
// add a dummy run element and replace it with the text
results.Inlines.Add(new Run(Replace));
string resultsXaml = XamlWriter.Save(results);
string resultsXamlWithText = resultsXaml.Replace(Replace, text);
// deserialise the xaml back into our TextBlock
results = XamlReader.Parse(resultsXamlWithText) as TextBlock;
}
return results.Inlines.ToList<Inline>();
}
// Not clear when this will be called but included for completeness
public object ConvertBack(
object value,
Type targetType,
object parameter,
System.Globalization.CultureInfo culture)
{
String results = String.Empty;
InlineCollection inlines = value as InlineCollection;
if (inlines != null)
{
//read the xaml as xml and return the "content"
var reader =
XElement.Parse(XamlWriter.Save(inlines)).CreateReader();
reader.MoveToContent();
results = reader.ReadInnerXml();
}
return results;
}
}
xaml:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:props="clr-namespace:Project.Properties"
xmlns:local="clr-namespace:Project">
<Window.Resources>
<props:Resources x:Key="Resources"/>
<local:StringInlineCollectionConvertor x:Key="InlineConvert"/>
</Window.Resources>
<local:BindableTextBlock InlineCollection="
{Binding Source={StaticResource Resources},
Path=FormattedText,
Converter={StaticResource InlineConvert}}"/>
</Window>
I made 2 classes. A sub-classed TextBlock with a "bindable" InlineCollection and an IValueConverter to convert the collection from and to a String.
Using InlineCollection directly as the type of the property made VS2010 complain, although the code still ran fine. I changed to a generic list of Inlines. I assume that there is an inherited attribute telling VS that the InlineCollection has no constructor.
I tryed making the InlineCollection property the BindableTextBlock's ContentProperty but ran into issues and out of time. Please feel free to take the next step and tell me about it.
I apologise for any errata but this code had to be transcribed and sanitised.
If there is a better way of doing this, surely there must be, please tell me that too. Wouldn't it be nice if this functionality was built in or, have I missed something?
I ended up needing to do this in my application and had to support many of the markup possible normally in TextBlock inlines, so I took Wallstreet Programmer's answer above (which works beautifully and is much less complicated than most other answers I found on this topic) and expanded on it. I figure someone else might find this useful.
I haven't thoroughly tested this with ALL the tags yet, but every one I've tested has worked like a charm. I also suspect it's not the fastest code in the world, but my own testing with several thousand formatted messages in a ListView seemed surprisingly zippy. YMMV. Code is below:
XAML:
<Window x:Class="FormatTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:FormatTest="clr-namespace:FormatTest"
Title="Window1" Height="300" Width="300">
<TextBlock FormatTest:FormattedTextBehavior.FormattedText="{Binding Path=Text}" />
</Window>
C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
namespace FormatTest
{
public static class FormattedTextBehavior
{
public class TextPart
{
public String mType = String.Empty;
public Inline mInline = null;
public InlineCollection mChildren = null;
public TextPart() {}
public TextPart(String t, Inline inline, InlineCollection col)
{
mType = t;
mInline = inline;
mChildren = col;
}
}
private static Regex mRegex = new Regex(#"<(?<Span>/?[^>]*)>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static Regex mSpanRegex = new Regex("(?<Key>[^\\s=]+)=\"(?<Val>[^\\s\"]*)\"", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(FormattedTextBehavior),
new UIPropertyMetadata("", FormattedTextChanged));
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
TextBlock textBlock = sender as TextBlock;
FormatText(e.NewValue as string, new TextPart("TextBlock", null, textBlock.Inlines));
}
public static void FormatText(String s, TextPart root)
{
int len = s.Length;
int lastIdx = 0;
List<TextPart> parts = new List<TextPart>();
parts.Add(root);
Match m = mRegex.Match(s);
while (m.Success)
{
String tag = m.Result("${Span}");
if (tag.StartsWith("/"))
{
String prevStr = s.Substring(lastIdx, m.Index - lastIdx);
TextPart part = parts.Last();
if (!String.IsNullOrEmpty(prevStr))
{
if (part.mChildren != null)
{
part.mChildren.Add(new Run(prevStr));
}
else if (part.mInline is Run)
{
(part.mInline as Run).Text = prevStr;
}
}
if (!tag.Substring(1).Equals(part.mType, StringComparison.InvariantCultureIgnoreCase))
{
Logger.LogD("Mismatched End Tag '" + tag.Substring(1) + "' (expected </" + part.mType + ">) at position " + m.Index.ToString() + " in String '" + s + "'");
}
if (parts.Count > 1)
{
parts.RemoveAt(parts.Count - 1);
TextPart parentPart = parts.Last();
if (parentPart.mChildren != null)
{
parentPart.mChildren.Add(part.mInline);
}
}
}
else
{
TextPart prevPart = parts.Last();
String prevStr = s.Substring(lastIdx, m.Index - lastIdx);
if (!String.IsNullOrEmpty(prevStr))
{
if (prevPart.mChildren != null)
{
prevPart.mChildren.Add(new Run(prevStr));
}
else if (prevPart.mInline is Run)
{
(prevPart.mInline as Run).Text = prevStr;
}
}
bool hasAttributes = false;
TextPart part = new TextPart();
if (tag.StartsWith("bold", StringComparison.InvariantCultureIgnoreCase))
{
part.mType = "BOLD";
part.mInline = new Bold();
part.mChildren = (part.mInline as Bold).Inlines;
}
else if (tag.StartsWith("underline", StringComparison.InvariantCultureIgnoreCase))
{
part.mType = "UNDERLINE";
part.mInline = new Underline();
part.mChildren = (part.mInline as Underline).Inlines;
}
else if (tag.StartsWith("italic", StringComparison.InvariantCultureIgnoreCase))
{
part.mType = "ITALIC";
part.mInline = new Italic();
part.mChildren = (part.mInline as Italic).Inlines;
}
else if (tag.StartsWith("linebreak", StringComparison.InvariantCultureIgnoreCase))
{
part.mType = "LINEBREAK";
part.mInline = new LineBreak();
}
else if (tag.StartsWith("span", StringComparison.InvariantCultureIgnoreCase))
{
hasAttributes = true;
part.mType = "SPAN";
part.mInline = new Span();
part.mChildren = (part.mInline as Span).Inlines;
}
else if (tag.StartsWith("run", StringComparison.InvariantCultureIgnoreCase))
{
hasAttributes = true;
part.mType = "RUN";
part.mInline = new Run();
}
else if (tag.StartsWith("hyperlink", StringComparison.InvariantCultureIgnoreCase))
{
hasAttributes = true;
part.mType = "HYPERLINK";
part.mInline = new Hyperlink();
part.mChildren = (part.mInline as Hyperlink).Inlines;
}
if (hasAttributes && part.mInline != null)
{
Match m2 = mSpanRegex.Match(tag);
while (m2.Success)
{
String key = m2.Result("${Key}");
String val = m2.Result("${Val}");
if (key.Equals("FontWeight", StringComparison.InvariantCultureIgnoreCase))
{
FontWeight fw = FontWeights.Normal;
try
{
fw = (FontWeight)new FontWeightConverter().ConvertFromString(val);
}
catch (Exception)
{
fw = FontWeights.Normal;
}
part.mInline.FontWeight = fw;
}
else if (key.Equals("FontSize", StringComparison.InvariantCultureIgnoreCase))
{
double fs = part.mInline.FontSize;
if (Double.TryParse(val, out fs))
{
part.mInline.FontSize = fs;
}
}
else if (key.Equals("FontStretch", StringComparison.InvariantCultureIgnoreCase))
{
FontStretch fs = FontStretches.Normal;
try
{
fs = (FontStretch)new FontStretchConverter().ConvertFromString(val);
}
catch (Exception)
{
fs = FontStretches.Normal;
}
part.mInline.FontStretch = fs;
}
else if (key.Equals("FontStyle", StringComparison.InvariantCultureIgnoreCase))
{
FontStyle fs = FontStyles.Normal;
try
{
fs = (FontStyle)new FontStyleConverter().ConvertFromString(val);
}
catch (Exception)
{
fs = FontStyles.Normal;
}
part.mInline.FontStyle = fs;
}
else if (key.Equals("FontFamily", StringComparison.InvariantCultureIgnoreCase))
{
if (!String.IsNullOrEmpty(val))
{
FontFamily ff = new FontFamily(val);
if (Fonts.SystemFontFamilies.Contains(ff))
{
part.mInline.FontFamily = ff;
}
}
}
else if (key.Equals("Background", StringComparison.InvariantCultureIgnoreCase))
{
Brush b = part.mInline.Background;
try
{
b = (Brush)new BrushConverter().ConvertFromString(val);
}
catch (Exception)
{
b = part.mInline.Background;
}
part.mInline.Background = b;
}
else if (key.Equals("Foreground", StringComparison.InvariantCultureIgnoreCase))
{
Brush b = part.mInline.Foreground;
try
{
b = (Brush)new BrushConverter().ConvertFromString(val);
}
catch (Exception)
{
b = part.mInline.Foreground;
}
part.mInline.Foreground = b;
}
else if (key.Equals("ToolTip", StringComparison.InvariantCultureIgnoreCase))
{
part.mInline.ToolTip = val;
}
else if (key.Equals("Text", StringComparison.InvariantCultureIgnoreCase) && part.mInline is Run)
{
(part.mInline as Run).Text = val;
}
else if (key.Equals("NavigateUri", StringComparison.InvariantCultureIgnoreCase) && part.mInline is Hyperlink)
{
(part.mInline as Hyperlink).NavigateUri = new Uri(val);
}
m2 = m2.NextMatch();
}
}
if (part.mInline != null)
{
if (tag.TrimEnd().EndsWith("/"))
{
if (prevPart.mChildren != null)
{
prevPart.mChildren.Add(part.mInline);
}
}
else
{
parts.Add(part);
}
}
}
lastIdx = m.Index + m.Length;
m = m.NextMatch();
}
if (lastIdx < (len - 1))
{
root.mChildren.Add(new Run(s.Substring(lastIdx)));
}
}
}
}
The same I have implemented using Behavior. Code given below:
public class FormatTextBlock : Behavior<System.Windows.Controls.TextBlock>
{
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.Register(
"FormattedText",
typeof(string),
typeof(FormatTextBlock),
new PropertyMetadata(string.Empty, OnFormattedTextChanged));
public string FormattedText
{
get { return (string)AssociatedObject.GetValue(FormattedTextProperty); }
set { AssociatedObject.SetValue(FormattedTextProperty, value); }
}
private static void OnFormattedTextChanged(DependencyObject textBlock, DependencyPropertyChangedEventArgs eventArgs)
{
System.Windows.Controls.TextBlock currentTxtBlock = (textBlock as FormatTextBlock).AssociatedObject;
string text = eventArgs.NewValue as string;
if (currentTxtBlock != null)
{
currentTxtBlock.Inlines.Clear();
string[] strs = text.Split(new string[] { "<Bold>", "</Bold>" }, StringSplitOptions.None);
for (int i = 0; i < strs.Length; i++)
{
currentTxtBlock.Inlines.Add(new Run { Text = strs[i], FontWeight = i % 2 == 1 ? FontWeights.Bold : FontWeights.Normal });
}
}
}
}
XAML - import namespace
<UserControl x:Class="MyClass"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:behav="clr-namespace:myAssembly.myNameSapce;assembly=myAssembly"
>
Then to use the behavior as:
<TextBlock TextWrapping="Wrap">
<i:Interaction.Behaviors>
<behav:FormatTextBlock FormattedText="{Binding Path=UIMessage}" />
</i:Interaction.Behaviors>
</TextBlock>
This work for me:
XAML:
<phone:PhoneApplicationPage x:Class="MyAPP.Views.Class"
xmlns:utils="clr-namespace:MyAPP.Utils">
and your TextBlock XAML:
<TextBlock utils:TextBlockHelper.FormattedText="{Binding Text}" />
CODE:
public static class TextBlockHelper
{
public static string GetFormattedText(DependencyObject textBlock)
{
return (string)textBlock.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject textBlock, string value)
{
textBlock.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText", typeof(string), typeof(TextBlock),
new PropertyMetadata(string.Empty, (sender, e) =>
{
string text = e.NewValue as string;
var textB1 = sender as TextBlock;
if (textB1 != null)
{
textB1.Inlines.Clear();
var str = text.Split(new string[] { "<b>", "</b>" }, StringSplitOptions.None);
for (int i = 0; i < str.Length; i++)
textB1.Inlines.Add(new Run { Text = str[i], FontWeight = i % 2 == 1 ? FontWeights.Bold : FontWeights.Normal });
}
}));
}
USE in your string binding:
String Text = Text <b>Bold</b>;
So, combining a behavior to get the attached property and Jodrells use of XamlReader, here's a version that can deal with most things you would expect to be able to have as inlines in a TextBlock. Only the default and x: namespaces supperted, but you could extend that.
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
"FormattedText",
typeof(string),
typeof(TextBlockBehaviour),
new PropertyMetadata(default(string), FormattedTextChanged_));
public static bool GetFormattedText(TextBlock textBlock)
{
return (bool)textBlock.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(TextBlock textBlock, bool value)
{
textBlock.SetValue(FormattedTextProperty, value);
}
private static void FormattedTextChanged_(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextBlock textBlock = d as TextBlock;
if (textBlock == null)
return;
textBlock.Inlines.Clear();
string value = e.NewValue as string;
if (string.IsNullOrEmpty(value))
{
textBlock.Text = null;
return;
}
using (var stringReader = new StringReader($"<TextBlock xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">{value}</TextBlock>"))
{
using (var xmlReader = XmlReader.Create(stringReader))
{
TextBlock newTextBlock = (TextBlock)XamlReader.Load(xmlReader);
if (newTextBlock.Inlines.Count == 0)
{
textBlock.Text = newTextBlock.Text;
}
else
{
foreach (var inline in newTextBlock.Inlines.ToArray())
{
textBlock.Inlines.Add(inline);
}
}
}
}
}

WPF Generate TextBlock Inlines

I have a GridView and in one of the GridViewColumns i want to generate a text like this:
textBlock.Text = string.Format("{0} is doing {1} .......", a, b);
but a and b (Properties of an item in the View) should not just be represented as plain text, but as a Hyperlink for example.
(Also: The format text should depend on the type of the item)
How can i generate the TextBlocks text in that way? (for localization)
The Question is more: Should i write something on my own or is there an easy way provided by the framework?
An old question, but I find accepted answer an absolute overkill. You don't need to parse the formatted text at all! Just wrap it up in Span element and you are done.
public class Attached
{
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
"FormattedText",
typeof(string),
typeof(Attached),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure, FormattedTextPropertyChanged));
public static void SetFormattedText(DependencyObject textBlock, string value)
{
textBlock.SetValue(FormattedTextProperty, value);
}
public static string GetFormattedText(DependencyObject textBlock)
{
return (string)textBlock.GetValue(FormattedTextProperty);
}
private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textBlock = d as TextBlock;
if (textBlock == null)
{
return;
}
var formattedText = (string)e.NewValue ?? string.Empty;
formattedText = string.Format("<Span xml:space=\"preserve\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">{0}</Span>", formattedText);
textBlock.Inlines.Clear();
using (var xmlReader = XmlReader.Create(new StringReader(formattedText)))
{
var result = (Span)XamlReader.Load(xmlReader);
textBlock.Inlines.Add(result);
}
}
}
Now you can use the FormattedText attached property either in your code:
string inlineExpression = "<Run Style=\"Theme.GrayText\">Once I saw a little bird, go hop, hop, hop.</Run>";
Attached.SetFormattedText(myTextBlock1, inlineExpression);
More importantly, straight from the XAML:
<TextBlock ns:Attached.FormattedText="{Binding Content}" />
Where ns is the namespace you defined the Attached class in.
Recently i came across the same problem. So i decided to implement an attached property for TextBlock which takes a value of string type and then populates the Inlines collection on the fly. You can simply set the property value to something like this:
string inlineExpression = "Once i saw a little <bold>bird</bold>, <span>go <bold>hop, hop, hop</bold></span>.";
InlineExpression.SetInlineExpression(myTextBlock1, inlineExpression);
The styles are also supported:
string inlineExpression = "<run style="Theme.GrayText">Once i saw a little bird, go hop, hop, hop.</run>";
InlineExpression.SetInlineExpression(myTextBlock1, inlineExpression);
You can also use this attached property in XAML in a standard way.
Here is the code of the class which exposes this property:
public class InlineExpression
{
public static readonly DependencyProperty InlineExpressionProperty = DependencyProperty.RegisterAttached(
"InlineExpression", typeof(string), typeof(TextBlock), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure));
public static void SetInlineExpression(TextBlock textBlock, string value)
{
textBlock.SetValue(InlineExpressionProperty, value);
textBlock.Inlines.Clear();
if (string.IsNullOrEmpty(value))
return;
var descriptions = GetInlineDescriptions(value);
if (descriptions.Length == 0)
return;
var inlines = GetInlines(textBlock, descriptions);
if (inlines.Length == 0)
return;
textBlock.Inlines.AddRange(inlines);
}
public static string GetInlineExpression(TextBlock textBlock)
{
return (string)textBlock.GetValue(InlineExpressionProperty);
}
enum InlineType
{
Run,
LineBreak,
Span,
Bold,
Italic,
Hyperlink,
Underline
}
class InlineDescription
{
public InlineType Type { get; set; }
public string Text { get; set; }
public InlineDescription[] Inlines { get; set; }
public string StyleName { get; set; }
}
private static Inline[] GetInlines(FrameworkElement element, IEnumerable<InlineDescription> inlineDescriptions)
{
var inlines = new List<Inline>();
foreach (var description in inlineDescriptions)
{
var inline = GetInline(element, description);
if (inline != null)
inlines.Add(inline);
}
return inlines.ToArray();
}
private static Inline GetInline(FrameworkElement element, InlineDescription description)
{
Style style = null;
if (!string.IsNullOrEmpty(description.StyleName))
{
style = element.FindResource(description.StyleName) as Style;
if (style == null)
throw new InvalidOperationException("The style '" + description.StyleName + "' cannot be found");
}
Inline inline = null;
switch (description.Type)
{
case InlineType.Run:
var run = new Run(description.Text);
inline = run;
break;
case InlineType.LineBreak:
var lineBreak = new LineBreak();
inline = lineBreak;
break;
case InlineType.Span:
var span = new Span();
inline = span;
break;
case InlineType.Bold:
var bold = new Bold();
inline = bold;
break;
case InlineType.Italic:
var italic = new Italic();
inline = italic;
break;
case InlineType.Hyperlink:
var hyperlink = new Hyperlink();
inline = hyperlink;
break;
case InlineType.Underline:
var underline = new Underline();
inline = underline;
break;
}
if (inline != null)
{
var span = inline as Span;
if (span != null)
{
var childInlines = new List<Inline>();
foreach (var inlineDescription in description.Inlines)
{
var childInline = GetInline(element, inlineDescription);
if (childInline == null)
continue;
childInlines.Add(childInline);
}
span.Inlines.AddRange(childInlines);
}
if (style != null)
inline.Style = style;
}
return inline;
}
private static InlineDescription[] GetInlineDescriptions(string inlineExpression)
{
if (inlineExpression == null)
return new InlineDescription[0];
inlineExpression = inlineExpression.Trim();
if (inlineExpression.Length == 0)
return new InlineDescription[0];
inlineExpression = inlineExpression.Insert(0, #"<root>");
inlineExpression = inlineExpression.Insert(inlineExpression.Length, #"</root>");
var xmlTextReader = new XmlTextReader(new StringReader(inlineExpression));
var xmlDocument = new XmlDocument();
xmlDocument.Load(xmlTextReader);
var rootElement = xmlDocument.DocumentElement;
if (rootElement == null)
return new InlineDescription[0];
var inlineDescriptions = new List<InlineDescription>();
foreach (XmlNode childNode in rootElement.ChildNodes)
{
var description = GetInlineDescription(childNode);
if (description == null)
continue;
inlineDescriptions.Add(description);
}
return inlineDescriptions.ToArray();
}
private static InlineDescription GetInlineDescription(XmlNode node)
{
var element = node as XmlElement;
if (element != null)
return GetInlineDescription(element);
var text = node as XmlText;
if (text != null)
return GetInlineDescription(text);
return null;
}
private static InlineDescription GetInlineDescription(XmlElement element)
{
InlineType type;
var elementName = element.Name.ToLower();
switch (elementName)
{
case "run":
type = InlineType.Run;
break;
case "linebreak":
type = InlineType.LineBreak;
break;
case "span":
type = InlineType.Span;
break;
case "bold":
type = InlineType.Bold;
break;
case "italic":
type = InlineType.Italic;
break;
case "hyperlink":
type = InlineType.Hyperlink;
break;
case "underline":
type = InlineType.Underline;
break;
default:
return null;
}
string styleName = null;
var attribute = element.GetAttributeNode("style");
if (attribute != null)
styleName = attribute.Value;
string text = null;
var childDescriptions = new List<InlineDescription>();
if (type == InlineType.Run || type == InlineType.LineBreak)
{
text = element.InnerText;
}
else
{
foreach (XmlNode childNode in element.ChildNodes)
{
var childDescription = GetInlineDescription(childNode);
if (childDescription == null)
continue;
childDescriptions.Add(childDescription);
}
}
var inlineDescription = new InlineDescription
{
Type = type,
StyleName = styleName,
Text = text,
Inlines = childDescriptions.ToArray()
};
return inlineDescription;
}
private static InlineDescription GetInlineDescription(XmlText text)
{
var value = text.Value;
if (string.IsNullOrEmpty(value))
return null;
var inlineDescription = new InlineDescription
{
Type = InlineType.Run,
Text = value
};
return inlineDescription;
}
}
In XAML you could do something like this:
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock>
<Hyperlink NavigateUri="{Binding AUri}">
<Run Text="{Binding A}"/>
</Hyperlink>
<Run Text=" is doing "/>
<Hyperlink NavigateUri="{Binding BUri}">
<Run Text="{Binding B}"/>
</Hyperlink>
</TextBlock>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
In code behind the same thing can be done but i would not recommend it since it involves using FrameworkElementFactories.

Resources