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);
}
}
}
}
}
Related
I'm making an editor in PropertyDrawer using SceneView.duringSceneGui. So it involves subscribing to SceneView.duringSceneGui when a property needs to draw stuff in SceneView and unsubscribing when it's gone. However I have no idea how to know if edited array element was removed from an array. It still exists in the memory and SceneView.duringSceneGui subscribed method is still there. I need to know when to stop editing and unsubscribe from it.
I guess I need to implement some context object, to store property value, edited object, PropertyDrawer and that subscription method should be there, to be able to unsubscribe exactly that editor... Although there may be only one editor running at once.
Does anybody found that out? Couldn't find anything with PropertyDrawers and array elements being deleted or removed.
TL.DR. Does Unity has an event to tell that PropertyDrawer's array element was removed or is there a simple or neat way to figure this out?
So, while making an example, I've solved my problem by getting property value every SceneView draw call and on catching an exception or if that value isn't edited stopped editor. I've added [NonSerialized] to _IsInEditMode to fix a new issue that I caught at the last moment, so that one is crucial.
Not sure if it's the best way to do that. If anybody will ever need to make a SceneView editor for some class, here's the example that works on arrays and lists also. Just separate it into 3 files and put them in respective folders, like Editor/ for MyClassDrawer.
using InspectorSerializedUtility;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
public class MyScript : MonoBehaviour
{
public MyClass field;
public List<MyClass> list = new List<MyClass>();
}
[Serializable]
public class MyClass
{
#if UNITY_EDITOR
[NonSerialized]
public bool _IsInEditMode;
#endif
public Vector3 position;
public void Reset()
{
position = Vector3.zero;
#if UNITY_EDITOR
_IsInEditMode = false;
#endif
}
}
[CustomPropertyDrawer(typeof(MyClass))]
public class MyClassDrawer : PropertyDrawer
{
public MyClass value;
public Transform targetTransform;
private Tool internalTool;
bool editorStarted {
get => value?._IsInEditMode ?? false;
set {
if (this.value != null)
this.value._IsInEditMode = value;
}
}
private SerializedProperty currentProperty;
private SerializedProperty drawerProperty;
private static MyClassDrawer currentlyEditedDrawer;
string editorButtonText(bool isInEditMode) => isInEditMode ? "Stop Editing" : "Start Editing";
Color editorButtonColor(bool isInEditMode) => isInEditMode ? Color.red + Color.white / 2f : Color.white;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
//Debug.Log("OnGUI");
drawerProperty = property;
targetTransform = ((Component)property.serializedObject.targetObject).transform;
var val = property.GetValue<MyClass>();
GUI.color = editorButtonColor(val._IsInEditMode);
var toggle = GUI.Toggle(position, val._IsInEditMode, editorButtonText(val._IsInEditMode), "Button");
if (toggle != val._IsInEditMode)
{
if (toggle && currentlyEditedDrawer != null && currentlyEditedDrawer.editorStarted)
currentlyEditedDrawer.StopEditor();
value = val;
currentlyEditedDrawer = this;
if (toggle)
StartEditor();
else
StopEditor();
}
GUI.color = Color.white;
}
public void OnDrawScene(SceneView sv) => OnDrawScene();
public void OnDrawScene()
{
//Debug.Log("OnDrawScene");
MyClass value = null;
try
{
value = currentProperty.GetValue<MyClass>();
if (!value._IsInEditMode)
{
StopEditor();
return;
}
} catch
{
StopEditor();
return;
}
var m = Handles.matrix;
Handles.matrix = targetTransform.localToWorldMatrix;
if (Tools.current == Tool.Move)
{
internalTool = Tool.Move;
Tools.current = Tool.None;
}
if (internalTool == Tool.Move)
{
var pos = Handles.PositionHandle(value.position, Quaternion.identity);
if (value.position != pos)
{
Undo.RecordObject(targetTransform, "position changed");
value.position = pos;
}
}
Handles.matrix = m;
}
public void StartEditor()
{
currentProperty = drawerProperty;
editorStarted = true;
Debug.Log("StartEditor");
Subscribe();
CallAllSceneViewRepaint();
}
private void Subscribe()
{
Unsubscribe();
SceneView.duringSceneGui += OnDrawScene;
Selection.selectionChanged += StopEditor;
EditorSceneManager.sceneClosed += StopEditor;
AssemblyReloadEvents.beforeAssemblyReload += StopEditor;
}
public void StopEditor(Scene s) => StopEditor();
public void StopEditor()
{
Tools.current = internalTool;
editorStarted = false;
Unsubscribe();
currentProperty = null;
CallAllSceneViewRepaint();
}
private void Unsubscribe()
{
SceneView.duringSceneGui -= OnDrawScene;
Selection.selectionChanged -= StopEditor;
EditorSceneManager.sceneClosed -= StopEditor;
AssemblyReloadEvents.beforeAssemblyReload -= StopEditor;
}
private void CallAllSceneViewRepaint()
{
foreach (SceneView sv in SceneView.sceneViews)
sv.Repaint();
}
}
namespace InspectorSerializedUtility
{
/// <summary>
/// https://gist.github.com/douduck08/6d3e323b538a741466de00c30aa4b61f
/// </summary>
public static class InspectorSeriallizedUtils
{
public static T GetValue<T>(this SerializedProperty property) where T : class
{
try
{
if (property.serializedObject.targetObject == null) return null;
}
catch
{
return null;
}
object obj = property.serializedObject.targetObject;
string path = property.propertyPath.Replace(".Array.data", "");
string[] fieldStructure = path.Split('.');
Regex rgx = new Regex(#"\[\d+\]");
for (int i = 0; i < fieldStructure.Length; i++)
{
if (fieldStructure[i].Contains("["))
{
int index = System.Convert.ToInt32(new string(fieldStructure[i].Where(c => char.IsDigit(c)).ToArray()));
obj = GetFieldValueWithIndex(rgx.Replace(fieldStructure[i], ""), obj, index);
}
else
{
obj = GetFieldValue(fieldStructure[i], obj);
}
}
return (T)obj;
}
public static bool SetValue<T>(this SerializedProperty property, T value) where T : class
{
object obj = property.serializedObject.targetObject;
string path = property.propertyPath.Replace(".Array.data", "");
string[] fieldStructure = path.Split('.');
Regex rgx = new Regex(#"\[\d+\]");
for (int i = 0; i < fieldStructure.Length - 1; i++)
{
if (fieldStructure[i].Contains("["))
{
int index = System.Convert.ToInt32(new string(fieldStructure[i].Where(c => char.IsDigit(c)).ToArray()));
obj = GetFieldValueWithIndex(rgx.Replace(fieldStructure[i], ""), obj, index);
}
else
{
obj = GetFieldValue(fieldStructure[i], obj);
}
}
string fieldName = fieldStructure.Last();
if (fieldName.Contains("["))
{
int index = System.Convert.ToInt32(new string(fieldName.Where(c => char.IsDigit(c)).ToArray()));
return SetFieldValueWithIndex(rgx.Replace(fieldName, ""), obj, index, value);
}
else
{
Debug.Log(value);
return SetFieldValue(fieldName, obj, value);
}
}
private static object GetFieldValue(string fieldName, object obj, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
{
FieldInfo field = obj.GetType().GetField(fieldName, bindings);
if (field != null)
{
return field.GetValue(obj);
}
return default(object);
}
private static object GetFieldValueWithIndex(string fieldName, object obj, int index, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
{
FieldInfo field = obj.GetType().GetField(fieldName, bindings);
if (field != null)
{
object list = field.GetValue(obj);
if (list.GetType().IsArray)
{
return ((object[])list)[index];
}
else if (list is IEnumerable)
{
return ((IList)list)[index];
}
}
return default(object);
}
public static bool SetFieldValue(string fieldName, object obj, object value, bool includeAllBases = false, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
{
FieldInfo field = obj.GetType().GetField(fieldName, bindings);
if (field != null)
{
field.SetValue(obj, value);
return true;
}
return false;
}
public static bool SetFieldValueWithIndex(string fieldName, object obj, int index, object value, bool includeAllBases = false, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
{
FieldInfo field = obj.GetType().GetField(fieldName, bindings);
if (field != null)
{
object list = field.GetValue(obj);
if (list.GetType().IsArray)
{
((object[])list)[index] = value;
return true;
}
else if (value is IEnumerable)
{
((IList)list)[index] = value;
return true;
}
}
return false;
}
}
}
I have some simple WPF (.NET 4.0) application that works differently on Windows 7 and Windows 8.1. If I print a document and select Landscape orientation in Print Dialog or in code as shown below, Windows 8.1 truncates it (on the printer) as it would be portrait. (I have not seen this effect before under Windows 7).
private void Print_Click(object sender, RoutedEventArgs e)
{
PrintDialog printDialog = new PrintDialog();
printDialog.PrintQueue = System.Printing.LocalPrintServer.GetDefaultPrintQueue();
printDialog.PrintTicket = printDialog.PrintQueue.DefaultPrintTicket;
printDialog.PrintTicket.PageOrientation = System.Printing.PageOrientation.Landscape;
if (printDialog.ShowDialog()??false)
{
//... ometed some code here
GridPaginator paginator = new GridPaginator(...);
printDialog.PrintDocument(paginator, "MyApp");
}
}
Any ideas, what was changed in Windows?
The trick to making this work is to set the PrintTicket.PageMediaSize property, using a newly-created PageMediaSize instance that specifies both the PageMediaSizeName and the width and height, with the dimensions translated to match the orientation.
PageMediaSizeName mediaSzNm = printDialog.PrintTicket.PageMediaSize.PageMediaSizeName.Value;
System.Windows.Size size = mediaSzNm.SizeInDips(printDialog.PrintTicket.PageOrientation.Value);
printDialog.PrintTicket.PageMediaSize = new PageMediaSize(mediaSzNm, size.Width, size.Height);
static public class PrintingHelper
{
public const int Dpi = 96;
// Get the page size, translated for page orientation
public static Size SizeInDips(this PageMediaSizeName name, PageOrientation orientation)
{
if (orientation == PageOrientation.Landscape)
return new Size(HeightInDips(name), WidthInDips(name));
return new Size(WidthInDips(name), HeightInDips(name));
}
public static double WidthInDips(this PageMediaSizeName name)
{
return WidthInInches(name) * Dpi;
}
public static double HeightInDips(this PageMediaSizeName name)
{
return HeightInInches(name) * Dpi;
}
public static double WidthInInches(this PageMediaSizeName name)
{
if ((name == PageMediaSizeName.NorthAmericaLegal) || (name == PageMediaSizeName.NorthAmericaLetter))
return 8.5;
return 0;
}
public static double HeightInInches(this PageMediaSizeName name)
{
if (name == PageMediaSizeName.NorthAmericaLegal)
return 14.0;
else if (name == PageMediaSizeName.NorthAmericaLetter)
return 11.0;
return 0;
}
}
Strange solution but works.
Get PrintDialog source code and save it in another class name. Delete internal references and ShowDialog method. (You can show dialog in your first object,then print with this)
My Edited Class :
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Printing;
using System.Security;
using System.Security.Permissions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Xps;
using System.Xml;
using MS.Internal.Printing;
using System.Windows.Xps.Serialization;
using System.Windows.Documents;
using System.Windows.Documents.Serialization; // WritingCompletedEventArgs
using MS.Internal.PresentationFramework;
namespace System.Windows.Controls
{
public class CustomPrintDialog
{
#region Constructors
[SecurityCritical]
public
CustomPrintDialog(
)
{
_dialogInvoked = false;
_printQueue = null;
_printTicket = null;
_isPrintableAreaWidthUpdated = false;
_isPrintableAreaHeightUpdated = false;
_pageRangeSelection = PageRangeSelection.AllPages;
_minPage = 1;
_maxPage = 9999;
_userPageRangeEnabled = false;
}
#endregion Constructors
#region Public properties
public PageRangeSelection PageRangeSelection
{
get
{
return _pageRangeSelection;
}
set
{
_pageRangeSelection = value;
}
}
public PageRange PageRange
{
get
{
return _pageRange;
}
set
{
if ((value.PageTo <= 0) || (value.PageFrom <= 0))
{
}
_pageRange = value;
if (_pageRange.PageFrom > _pageRange.PageTo)
{
int temp = _pageRange.PageFrom;
_pageRange.PageFrom = _pageRange.PageTo;
_pageRange.PageTo = temp;
}
}
}
public bool UserPageRangeEnabled
{
get
{
return _userPageRangeEnabled;
}
set
{
_userPageRangeEnabled = value;
}
}
public UInt32 MinPage
{
get
{
return _minPage;
}
set
{
if (_minPage <= 0)
{
}
_minPage = value;
}
}
public UInt32 MaxPage
{
get
{
return _maxPage;
}
set
{
if (_maxPage <= 0)
{
}
_maxPage = value;
}
}
#pragma warning restore 3003
public PrintQueue PrintQueue
{
[SecurityCritical]
get
{
if (_printQueue == null)
{
_printQueue = AcquireDefaultPrintQueue();
}
return _printQueue;
}
[SecurityCritical]
set
{
_printQueue = value;
}
}
public PrintTicket PrintTicket
{
[SecurityCritical]
get
{
if (_printTicket == null)
{
_printTicket = AcquireDefaultPrintTicket(this.PrintQueue);
}
return _printTicket;
}
[SecurityCritical]
set
{
_printTicket = value;
}
}
public
double
PrintableAreaWidth
{
get
{
if (((_isPrintableAreaWidthUpdated == false) && (_isPrintableAreaHeightUpdated == false)) ||
((_isPrintableAreaWidthUpdated == true) && (_isPrintableAreaHeightUpdated == false)))
{
_isPrintableAreaWidthUpdated = true;
_isPrintableAreaHeightUpdated = false;
UpdatePrintableAreaSize();
}
return _printableAreaWidth;
}
}
public
double
PrintableAreaHeight
{
get
{
if (((_isPrintableAreaWidthUpdated == false) && (_isPrintableAreaHeightUpdated == false)) ||
((_isPrintableAreaWidthUpdated == false) && (_isPrintableAreaHeightUpdated == true)))
{
_isPrintableAreaWidthUpdated = false;
_isPrintableAreaHeightUpdated = true;
UpdatePrintableAreaSize();
}
return _printableAreaHeight;
}
}
#endregion Public properties
#region Public methods
[SecurityCritical]
public
void
PrintVisual(
Visual visual,
String description
)
{
if (visual == null)
{
throw new ArgumentNullException("visual");
}
XpsDocumentWriter writer = CreateWriter(description);
writer.Write(visual);
_printableAreaWidth = 0;
_printableAreaHeight = 0;
_isPrintableAreaWidthUpdated = false;
_isPrintableAreaHeightUpdated = false;
_dialogInvoked = false;
}
[SecurityCritical]
public
void
PrintDocument(
DocumentPaginator documentPaginator,
String description
)
{
if (documentPaginator == null)
{
throw new ArgumentNullException("documentPaginator");
}
XpsDocumentWriter writer = CreateWriter(description);
writer.Write(documentPaginator);
_printableAreaWidth = 0;
_printableAreaHeight = 0;
_isPrintableAreaWidthUpdated = false;
_isPrintableAreaHeightUpdated = false;
_dialogInvoked = false;
}
#endregion Public methods
[SecurityCritical]
private
PrintQueue
AcquireDefaultPrintQueue()
{
PrintQueue printQueue = null;
try
{
try
{
LocalPrintServer server = new LocalPrintServer();
printQueue = server.DefaultPrintQueue;
}
catch (PrintSystemException)
{
//
// It is entirely possible for there to be no "default" printer. In this case,
// the printing system throws an exception. We do not want this to propagate
// up. Instead, returning null is fine.
//
printQueue = null;
}
}
finally
{
}
return printQueue;
}
[SecurityCritical]
private
PrintTicket
AcquireDefaultPrintTicket(
PrintQueue printQueue
)
{
PrintTicket printTicket = null;
try
{
try
{
if (printQueue != null)
{
printTicket = printQueue.UserPrintTicket;
if (printTicket == null)
{
printTicket = printQueue.DefaultPrintTicket;
}
}
}
catch (PrintSystemException)
{
//
// The printing subsystem can throw an exception in certain cases when
// the print ticket is unavailable. If it does we will handle this
// below. There is no real need to bubble this up to the application.
//
printTicket = null;
}
}
finally
{
}
//
// If the printing subsystem could not give us a print ticket either due to
// a failure or because a user/system default was not available, then just
// create a blank/empty one.
//
if (printTicket == null)
{
printTicket = new PrintTicket();
}
return printTicket;
}
[SecurityCritical, SecurityTreatAsSafe]
private
void
UpdatePrintableAreaSize(
)
{
PrintQueue printQueue = null;
PrintTicket printTicket = null;
PickCorrectPrintingEnvironment(ref printQueue, ref printTicket);
PrintCapabilities printCap = null;
if (printQueue != null)
{
printCap = printQueue.GetPrintCapabilities(printTicket);
}
// PrintCapabilities OrientedPageMediaWidth/Height are Nullable
if ((printCap != null) &&
(printCap.OrientedPageMediaWidth != null) &&
(printCap.OrientedPageMediaHeight != null))
{
_printableAreaWidth = (double)printCap.OrientedPageMediaWidth;
_printableAreaHeight = (double)printCap.OrientedPageMediaHeight;
}
else
{
// Initialize page size to portrait Letter size.
// This is our fallback if PrintTicket doesn't specify the page size.
_printableAreaWidth = 816;
_printableAreaHeight = 1056;
// PrintTicket's PageMediaSize could be null and PageMediaSize Width/Height are Nullable
if ((printTicket.PageMediaSize != null) &&
(printTicket.PageMediaSize.Width != null) &&
(printTicket.PageMediaSize.Height != null))
{
_printableAreaWidth = (double)printTicket.PageMediaSize.Width;
_printableAreaHeight = (double)printTicket.PageMediaSize.Height;
}
// If we are using PrintTicket's PageMediaSize dimensions to populate the widht/height values,
// we need to adjust them based on current orientation. PrintTicket's PageOrientation is Nullable.
if (printTicket.PageOrientation != null)
{
PageOrientation orientation = (PageOrientation)printTicket.PageOrientation;
// need to swap width/height in landscape orientation
if ((orientation == PageOrientation.Landscape) ||
(orientation == PageOrientation.ReverseLandscape))
{
double t = _printableAreaWidth;
_printableAreaWidth = _printableAreaHeight;
_printableAreaHeight = t;
}
}
}
}
[SecurityCritical, SecurityTreatAsSafe]
private
XpsDocumentWriter
CreateWriter(
String description
)
{
PrintQueue printQueue = null;
PrintTicket printTicket = null;
XpsDocumentWriter writer = null;
PickCorrectPrintingEnvironment(ref printQueue, ref printTicket);
try
{
if (printQueue != null)
{
printQueue.CurrentJobSettings.Description = description;
}
writer = PrintQueue.CreateXpsDocumentWriter(printQueue);
PrintDlgPrintTicketEventHandler eventHandler = new PrintDlgPrintTicketEventHandler(printTicket);
writer.WritingPrintTicketRequired +=
new WritingPrintTicketRequiredEventHandler(eventHandler.SetPrintTicket);
}
finally
{
}
return writer;
}
[SecurityCritical]
private
void
PickCorrectPrintingEnvironment(
ref PrintQueue printQueue,
ref PrintTicket printTicket
)
{
if (_dialogInvoked == false)
{
//
// If the dialog has not been invoked then the user needs printing permissions.
// If the demand succeeds then they can print. If the demand fails, then we
// tell them that the print dialog must be displayed first by throwing a dialog
// exception.
//
try
{
}
catch (SecurityException)
{
}
}
//
// If the default print queue and print ticket have not already
// been selected then update them now since we need them.
//
// NOTE: If this code gets called then we know the dialog has never
// been invoked but the above demand was satisfied. In this
// case we want to just pickup the user defaults.
//
if (_printQueue == null)
{
_printQueue = AcquireDefaultPrintQueue();
}
if (_printTicket == null)
{
_printTicket = AcquireDefaultPrintTicket(_printQueue);
}
//
// We should have valid print queue and print ticket objects to
// return now. As a note, a null PrintQueue is valid for this
// since the dialog will automatically pick up the user default
// printer for us.
//
printQueue = _printQueue;
printTicket = _printTicket;
}
#region Private data
/// <SecurityNote>
/// The PrintTicket is critical and not obtainable from a partial
/// trust application unless they can satisfy a printing permission
/// demand.
/// </SecurityNote>
[SecurityCritical]
private
PrintTicket _printTicket;
/// <SecurityNote>
/// The PrintQueue is critical and not obtainable from a partial
/// trust application unless they can satisfy a printing permission
/// demand.
/// </SecurityNote>
[SecurityCritical]
private
PrintQueue _printQueue;
/// <SecurityNote>
/// This variable is used to determine whether a user actually invoked
/// and dismissed the dialog prior to printing. In a partial trust app,
/// we can safely perform the necessary asserts to print as long as the
/// user said printing was okay.
/// </SecurityNote>
[SecurityCritical]
private
bool _dialogInvoked;
private
PageRangeSelection _pageRangeSelection;
private
PageRange _pageRange;
private
bool _userPageRangeEnabled;
private
UInt32 _minPage;
private
UInt32 _maxPage;
private
double _printableAreaWidth;
private
double _printableAreaHeight;
private
bool _isPrintableAreaWidthUpdated;
private
bool _isPrintableAreaHeightUpdated;
#endregion Private data
#region Internal classes
internal class PrintDlgPrintTicketEventHandler
{
#region Constructor
/// <SecurityNote>
/// Critical - PrintTicket argument is critical because it is defined in the none APTCA assembly ReachFramework.dll
/// TreatAsSafe - PrintTicket type is safe
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
public
PrintDlgPrintTicketEventHandler(
PrintTicket printTicket
)
{
_printTicket = printTicket;
}
#endregion Constructor
#region Public Methods
/// <SecurityNote>
/// Critical - Makes use of PrintTicket type which is critical because it is defined in the none APTCA assembly ReachFramework.dll
/// - Makes use of PrintTicketLevel type which is critical because it is defined in the none APTCA assembly ReachFramework.dll
/// TreatAsSafe - PrintTicket type is safe
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
public
void
SetPrintTicket(
Object sender,
WritingPrintTicketRequiredEventArgs args
)
{
if (args.CurrentPrintTicketLevel == PrintTicketLevel.FixedDocumentSequencePrintTicket)
{
args.CurrentPrintTicket = _printTicket;
}
}
#endregion Public Methods
#region Private Data
private
PrintTicket _printTicket;
#endregion Private Data
};
#endregion Internal classes
}
}
Usage
var pd = new CustomPrintDialog
{
PrintQueue = new PrintQueue(new PrintServer(), "THERMAL Receipt Printer"),
PrintTicket = { PageOrientation = PageOrientation.Portrait }
};
pd.PrintVisual(this, "asdsa");
Can anyone explain how it works?
I have a WPF combobox like this :
<ComboBox x:Name="CustomerComboBox" IsEditable="True" ItemsSource="{Binding Relations.View}"
DisplayMemberPath="Model.sname" />
If I click in the editable combo while the binding is in place (MVVM) to give it focus, and I then press and hold any key, I assume the combo will be filled with that key rather quickly, but it isn't.
If I remove the displaymemberpath and then do the same, then I have the expected behavior. Of course I really need the binding.
The performance penalty only shows when the combo has a lot of elements mine has 6000.
I cannot understand where this performance penalty is coming from. Is there any way to bypass this problem ?
Below code solves the issues by creating a specialised combobox that basically caches all binding results. I created it by looking at the orginal source code of the combobox and itemscontrol using .NET reflector.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Reflection;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Windows.Controls.Primitives;
using System.Collections;
namespace ICeTechControlLibrary
{
public class FastEditComboBox : ComboBox
{
//PARTS
private TextBox _TextBoxPart = null;
//DEPENDENCY PROPERTIES
public static readonly DependencyProperty TextProperty
= DependencyProperty.Register("Text", typeof(string), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.Journal | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(FastEditComboBox.OnTextChanged)));
private List<string> _CompletionStrings = new List<string>();
private int _textBoxSelectionStart;
private bool _updatingText;
private bool _updatingSelectedItem;
private static Dictionary<TextBox, FastEditComboBox> _TextBoxDictionary = new Dictionary<TextBox,FastEditComboBox>();
static FastEditComboBox()
{
EventManager.RegisterClassHandler(typeof(TextBox), TextBox.TextChangedEvent, new TextChangedEventHandler(FastEditComboBox.OnTextChanged));
EventManager.RegisterClassHandler(typeof(TextBox), TextBox.SelectionChangedEvent, new RoutedEventHandler(FastEditComboBox.OnSelectionChanged));
}
public string Text
{
get
{
return (string)base.GetValue(TextProperty);
}
set
{
base.SetValue(TextProperty, value);
}
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_TextBoxPart = base.GetTemplateChild("PART_EditableTextBox") as TextBox;
if (!_TextBoxDictionary.ContainsKey(_TextBoxPart)) _TextBoxDictionary.Add(_TextBoxPart, this);
}
private void OnTextBoxSelectionChanged(object sender, RoutedEventArgs e)
{
this._textBoxSelectionStart = this._TextBoxPart.SelectionStart;
}
private void OnTextBoxTextChanged(object sender, TextChangedEventArgs e)
{
if (IsEditable)
{
TextUpdated(_TextBoxPart.Text, true);
}
}
private void TextUpdated(string newText, bool textBoxUpdated)
{
if (!_updatingText && !_updatingSelectedItem)
{
try
{
_updatingText = true;
if (base.IsTextSearchEnabled)
{
int num = FindMatchingPrefix(newText);
if (num >= 0)
{
if (textBoxUpdated)
{
int selectionStart = this._TextBoxPart.SelectionStart;
if ((selectionStart == newText.Length) && (selectionStart > this._textBoxSelectionStart))
{
string primaryTextFromItem = _CompletionStrings[num];
this._TextBoxPart.Text = primaryTextFromItem;
this._TextBoxPart.SelectionStart = newText.Length;
this._TextBoxPart.SelectionLength = primaryTextFromItem.Length - newText.Length;
newText = primaryTextFromItem;
}
}
else
{
string b = _CompletionStrings[num];
if (!string.Equals(newText, b, StringComparison.CurrentCulture))
{
num = -1;
}
}
}
if (num != base.SelectedIndex)
{
SelectedIndex = num;
}
}
if (textBoxUpdated)
{
Text = newText;
}
else if (_TextBoxPart != null)
{
_TextBoxPart.Text = newText;
}
}
finally
{
_updatingText = false;
}
}
}
internal void SelectedItemUpdated()
{
try
{
this._updatingSelectedItem = true;
if (!this._updatingText)
{
string primaryTextFromItem = GetPrimaryTextFromItem(SelectedItem);
Text = primaryTextFromItem;
}
this.Update();
}
finally
{
this._updatingSelectedItem = false;
}
}
private void Update()
{
if (this.IsEditable)
{
this.UpdateEditableTextBox();
}
else
{
//this.UpdateSelectionBoxItem();
}
}
private void UpdateEditableTextBox()
{
if (!_updatingText)
{
try
{
this._updatingText = true;
string text = this.Text;
if ((this._TextBoxPart != null) && (this._TextBoxPart.Text != text))
{
this._TextBoxPart.Text = text;
this._TextBoxPart.SelectAll();
}
}
finally
{
this._updatingText = false;
}
}
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.RaiseEvent(e);
this.SelectedItemUpdated();
if (this.IsDropDownOpen)
{
object Item = SelectedItem;
if (Item != null)
{
base.OnSelectionChanged(e);
}
//object internalSelectedItem = base.InternalSelectedItem;
//if (internalSelectedItem != null)
//{
// base.NavigateToItem(internalSelectedItem, ItemsControl.ItemNavigateArgs.Empty);
//}
}
}
int FindMatchingPrefix(string s)
{
int index = _CompletionStrings.BinarySearch(s, StringComparer.OrdinalIgnoreCase);
if (index >= 0) return index;
index = ~index;
string p = _CompletionStrings[index];
if (p.StartsWith(s, StringComparison.CurrentCultureIgnoreCase)) return index;
return -1;
}
protected override void OnDisplayMemberPathChanged(string oldDisplayMemberPath, string newDisplayMemberPath)
{
FillCompletionStrings();
}
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
switch (e.Action)
{
case System.Collections.Specialized.NotifyCollectionChangedAction.Add:
AddCompletionStrings(e.NewItems);
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Remove:
RemoveCompletionStrings(e.OldItems);
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Reset:
FillCompletionStrings();
break;
}
}
private void FillCompletionStrings()
{
_CompletionStrings.Clear();
AddCompletionStrings(Items);
}
private void RemoveCompletionStrings(IList items)
{
foreach (object o in items)
{
RemoveCompletionStringForItem(o);
}
}
private void AddCompletionStrings(IList items)
{
foreach (object o in items)
{
AddCompletionStringForItem(o);
}
}
private void AddCompletionStringForItem(object item)
{
Binding binding = new Binding(DisplayMemberPath);
TextBlock tb = new TextBlock();
tb.DataContext = item;
tb.SetBinding(TextBlock.TextProperty, binding);
string s = tb.Text;
int index = _CompletionStrings.BinarySearch(s, StringComparer.OrdinalIgnoreCase);
if (index < 0)
{
_CompletionStrings.Insert(~index, s);
}
else
{
_CompletionStrings.Insert(index, s);
}
}
private string GetPrimaryTextFromItem(object item)
{
Binding binding = new Binding(DisplayMemberPath);
TextBlock tb = new TextBlock();
tb.DataContext = item;
tb.SetBinding(TextBlock.TextProperty, binding);
string s = tb.Text;
return s;
}
private void RemoveCompletionStringForItem(object item)
{
Binding binding = new Binding(DisplayMemberPath);
TextBlock tb = new TextBlock();
tb.DataContext = item;
tb.SetBinding(TextBlock.TextProperty, binding);
string s = tb.Text;
int index = _CompletionStrings.BinarySearch(s, StringComparer.OrdinalIgnoreCase);
if (index >= 0) _CompletionStrings.RemoveAt(index);
}
private static void OnTextChanged(object sender, TextChangedEventArgs e)
{
TextBox tb = e.Source as TextBox;
if (tb.Name == "PART_EditableTextBox")
{
if (_TextBoxDictionary.ContainsKey(tb))
{
FastEditComboBox combo = _TextBoxDictionary[tb];
combo.OnTextBoxTextChanged(sender, e);
e.Handled = true;
}
}
}
private static void OnSelectionChanged(object sender, RoutedEventArgs e)
{
TextBox tb = e.Source as TextBox;
if (tb.Name == "PART_EditableTextBox")
{
if (_TextBoxDictionary.ContainsKey(tb))
{
FastEditComboBox combo = _TextBoxDictionary[tb];
combo.OnTextBoxSelectionChanged(sender, e);
e.Handled = true;
}
}
}
private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FastEditComboBox actb = (FastEditComboBox)d;
actb.TextUpdated((string)e.NewValue, false);
}
}
}
selecting the very first element clears the selection with this implementation.
so still some bugs here
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.
I'm trying to databind a Button's IsMouseOver read-only dependency property to a boolean read/write property in my view model.
Basically I need the Button's IsMouseOver property value to be read to a view model's property.
<Button IsMouseOver="{Binding Path=IsMouseOverProperty, Mode=OneWayToSource}" />
I'm getting a compile error: 'IsMouseOver' property is read-only and cannot be set from markup. What am I doing wrong?
No mistake. This is a limitation of WPF - a read-only property cannot be bound OneWayToSource unless the source is also a DependencyProperty.
An alternative is an attached behavior.
As many people have mentioned, this is a bug in WPF and the best way is to do it is attached property like Tim/Kent suggested. Here is the attached property I use in my project. I intentionally do it this way for readability, unit testability, and sticking to MVVM without codebehind on the view to handle the events manually everywhere.
public interface IMouseOverListener
{
void SetIsMouseOver(bool value);
}
public static class ControlExtensions
{
public static readonly DependencyProperty MouseOverListenerProperty =
DependencyProperty.RegisterAttached("MouseOverListener", typeof (IMouseOverListener), typeof (ControlExtensions), new PropertyMetadata(OnMouseOverListenerChanged));
private static void OnMouseOverListenerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var element = ((UIElement)d);
if(e.OldValue != null)
{
element.MouseEnter -= ElementOnMouseEnter;
element.MouseLeave -= ElementOnMouseLeave;
}
if(e.NewValue != null)
{
element.MouseEnter += ElementOnMouseEnter;
element.MouseLeave += ElementOnMouseLeave;
}
}
public static void SetMouseOverListener(UIElement element, IMouseOverListener value)
{
element.SetValue(MouseOverListenerProperty, value);
}
public static IMouseOverListener GetMouseOverListener(UIElement element)
{
return (IMouseOverListener) element.GetValue(MouseOverListenerProperty);
}
private static void ElementOnMouseLeave(object sender, MouseEventArgs mouseEventArgs)
{
var element = ((UIElement)sender);
var listener = GetMouseOverListener(element);
if(listener != null)
listener.SetIsMouseOver(false);
}
private static void ElementOnMouseEnter(object sender, MouseEventArgs mouseEventArgs)
{
var element = ((UIElement)sender);
var listener = GetMouseOverListener(element);
if (listener != null)
listener.SetIsMouseOver(true);
}
}
Here's a rough draft of what i resorted to while seeking a general solution to this problem. It employs a css-style formatting to specify Dependency-Properties to be bound to model properties (models gotten from the DataContext); this also means it will work only on FrameworkElements.
I haven't thoroughly tested it, but the happy path works just fine for the few test cases i ran.
public class BindingInfo
{
internal string sourceString = null;
public DependencyProperty source { get; internal set; }
public string targetProperty { get; private set; }
public bool isResolved => source != null;
public BindingInfo(string source, string target)
{
this.sourceString = source;
this.targetProperty = target;
validate();
}
private void validate()
{
//verify that targetProperty is a valid c# property access path
if (!targetProperty.Split('.')
.All(p => Identifier.IsMatch(p)))
throw new Exception("Invalid target property - " + targetProperty);
//verify that sourceString is a [Class].[DependencyProperty] formatted string.
if (!sourceString.Split('.')
.All(p => Identifier.IsMatch(p)))
throw new Exception("Invalid source property - " + sourceString);
}
private static readonly Regex Identifier = new Regex(#"[_a-z][_\w]*$", RegexOptions.IgnoreCase);
}
[TypeConverter(typeof(BindingInfoConverter))]
public class BindingInfoGroup
{
private List<BindingInfo> _infoList = new List<BindingInfo>();
public IEnumerable<BindingInfo> InfoList
{
get { return _infoList.ToArray(); }
set
{
_infoList.Clear();
if (value != null) _infoList.AddRange(value);
}
}
}
public class BindingInfoConverter: TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string)) return true;
return base.CanConvertFrom(context, sourceType);
}
// Override CanConvertTo to return true for Complex-to-String conversions.
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string)) return true;
return base.CanConvertTo(context, destinationType);
}
// Override ConvertFrom to convert from a string to an instance of Complex.
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string text = value as string;
return new BindingInfoGroup
{
InfoList = text.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(binfo =>
{
var parts = binfo.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2) throw new Exception("invalid binding info - " + binfo);
return new BindingInfo(parts[0].Trim(), parts[1].Trim());
})
};
}
// Override ConvertTo to convert from an instance of Complex to string.
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture,
object value, Type destinationType)
{
var bgroup = value as BindingInfoGroup;
return bgroup.InfoList
.Select(bi => $"{bi.sourceString}:{bi.targetProperty};")
.Aggregate((n, p) => n += $"{p} ")
.Trim();
}
public override bool GetStandardValuesSupported(ITypeDescriptorContext context) => false;
}
public class Bindings
{
#region Fields
private static ConcurrentDictionary<DependencyProperty, PropertyChangeHandler> _Properties =
new ConcurrentDictionary<DependencyProperty, PropertyChangeHandler>();
#endregion
#region OnewayBindings
public static readonly DependencyProperty OnewayBindingsProperty =
DependencyProperty.RegisterAttached("OnewayBindings", typeof(BindingInfoGroup), typeof(Bindings), new FrameworkPropertyMetadata
{
DefaultValue = null,
PropertyChangedCallback = (x, y) =>
{
var fwe = x as FrameworkElement;
if (fwe == null) return;
//resolve the bindings
resolve(fwe);
//add change delegates
(GetOnewayBindings(fwe)?.InfoList ?? new BindingInfo[0])
.Where(bi => bi.isResolved)
.ToList()
.ForEach(bi =>
{
var descriptor = DependencyPropertyDescriptor.FromProperty(bi.source, fwe.GetType());
PropertyChangeHandler listener = null;
if (_Properties.TryGetValue(bi.source, out listener))
{
descriptor.RemoveValueChanged(fwe, listener.callback); //cus there's no way to check if it had one before...
descriptor.AddValueChanged(fwe, listener.callback);
}
});
}
});
private static void resolve(FrameworkElement element)
{
var bgroup = GetOnewayBindings(element);
bgroup.InfoList
.ToList()
.ForEach(bg =>
{
//source
var sourceParts = bg.sourceString.Split('.');
if (sourceParts.Length == 1)
{
bg.source = element.GetType()
.baseTypes() //<- flattens base types, including current type
.SelectMany(t => t.GetRuntimeFields()
.Where(p => p.IsStatic)
.Where(p => p.FieldType == typeof(DependencyProperty)))
.Select(fi => fi.GetValue(null) as DependencyProperty)
.FirstOrDefault(dp => dp.Name == sourceParts[0])
.ThrowIfNull($"Dependency Property '{sourceParts[0]}' was not found");
}
else
{
//resolve the dependency property [ClassName].[PropertyName]Property - e.g FrameworkElement.DataContextProperty
bg.source = Type.GetType(sourceParts[0])
.GetField(sourceParts[1])
.GetValue(null)
.ThrowIfNull($"Dependency Property '{bg.sourceString}' was not found") as DependencyProperty;
}
_Properties.GetOrAdd(bg.source, ddp => new PropertyChangeHandler { property = ddp }); //incase it wasnt added before.
});
}
public static BindingInfoGroup GetOnewayBindings(FrameworkElement source)
=> source.GetValue(OnewayBindingsProperty) as BindingInfoGroup;
public static void SetOnewayBindings(FrameworkElement source, string value)
=> source.SetValue(OnewayBindingsProperty, value);
#endregion
}
public class PropertyChangeHandler
{
internal DependencyProperty property { get; set; }
public void callback(object obj, EventArgs args)
{
var fwe = obj as FrameworkElement;
var target = fwe.DataContext;
if (fwe == null) return;
if (target == null) return;
var bg = Bindings.GetOnewayBindings(fwe);
if (bg == null) return;
else bg.InfoList
.Where(bi => bi.isResolved)
.Where(bi => bi.source == property)
.ToList()
.ForEach(bi =>
{
//transfer data to the object
var data = fwe.GetValue(property);
KeyValuePair<object, PropertyInfo>? pinfo = resolveProperty(target, bi.targetProperty);
if (pinfo == null) return;
else pinfo.Value.Value.SetValue(pinfo.Value.Key, data);
});
}
private KeyValuePair<object, PropertyInfo>? resolveProperty(object target, string path)
{
try
{
var parts = path.Split('.');
if (parts.Length == 1) return new KeyValuePair<object, PropertyInfo>(target, target.GetType().GetProperty(parts[0]));
else //(parts.Length>1)
return resolveProperty(target.GetType().GetProperty(parts[0]).GetValue(target),
string.Join(".", parts.Skip(1)));
}
catch (Exception e) //too lazy to care :D
{
return null;
}
}
}
And to use the XAML...
<Grid ab:Bindings.OnewayBindings="IsMouseOver:mouseOver;">...</Grid>