Как связать TextBlock с ресурсом, содержащим форматированный текст?

У меня есть TextBlock в моем окне WPF.

 <TextBlock>
     Some <Bold>formatted</Bold> text.
 </TextBlock>

Когда он отображается, он выглядит так:

Некоторый отформатированный текст.

Мой вопрос: могу ли я привязать этот встроенный "контент" к ресурсу в моем приложении?

Дошел до:

Создание строки ресурса приложения,

myText="Some <Bold>formatted</Bold> text."

и следующий xaml (Некоторый код опущен для краткости)

 <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 отображает теги на месте и не выполняет форматирование.

Отформатированный текст <Bold> <Bold> .

Try2 не будет компилировать или визуализировать, потому что ресурс "myText" не имеет типа Inline, а строки.

Возможно ли это, казалось бы, простая задача, и если да, то как?

Ответ 1

Вот мой модифицированный код для рекурсивного формата текста. Он обрабатывает Bold, Italic, Underline и LineBreak, но может быть легко расширен для поддержки большего (измените оператор switch).

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

Изменить: (предложено Spook)

Более короткая версия, но требует, чтобы текст был XML-допустимым:

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

И пример использования:

<RootItem xmlns:u="clr-namespace:MyApp.Helpers">
    <TextBlock u:TextBlockHelper.FormattedText="{Binding SomeProperty}" />
</RootItem>

Ответ 2

Как насчет использования приложенного поведения? Ниже кода обрабатываются только жирные теги. Каждое слово, которое должно быть выделено жирным шрифтом, должно быть завернуто жирным тегом. Вероятно, вы хотите, чтобы класс принимал и другие форматы. Также нужно лучше обрабатывать пробелы, класс вытесняет последовательные пробелы и добавляет один дополнительный конец. Поэтому рассмотрим ниже класс как демонстрационный код, который потребует дальнейшей работы, чтобы быть полезной, но он должен начать вас.

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>

Код позади:

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 + " "));
                }
            }
        }
    }
}

Ответ 3

EDIT:

Эта строка,

<props:Resources x:Key="Resources"/>

- плохой подход к доступу к пространству имен Project.Properties.Resources. Это вызывает неудобные сбои при перекомпиляции.

Намного лучше использовать x:Static, чтобы сделать что-то вроде этого,

Text="{x:Static props:Resources.SomeText}"

в вашей привязке. спасибо to Бен


Хорошо, вот как я это сделал. Это не идеально, но оно работает.

Помните, что есть ресурс проекта, называемый 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>

Я сделал 2 класса. Подклассифицированный TextBlock с "привязываемым" InlineCollection и IValueConverter для преобразования коллекции из и в строку.

Использование InlineCollection непосредственно как тип свойства, сделанного VS2010, жалуется, хотя код все еще работает нормально. Я перешел на общий список Inlines. Я предполагаю, что есть унаследованный атрибут, сообщающий VS, что InlineCollection не имеет конструктора.

Я попытался сделать свойство InlineCollection BindableTextBlock ContentProperty, но столкнулся с проблемами и временем. Пожалуйста, не стесняйтесь сделать следующий шаг и рассказать мне об этом.

Извиняюсь за любые ошибки, но этот код пришлось переписывать и дезинфицировать.

Если есть лучший способ сделать это, конечно, должно быть, пожалуйста, скажите мне это тоже. Было бы неплохо, если бы эта функциональность была встроена или я пропустил что-то?

Ответ 4

Мне пришлось сделать это в своем приложении, и мне пришлось поддерживать многие из разметки, которые обычно можно было использовать в строках TextBlock, поэтому я принял решение Wallstreet Programmer выше (что работает красиво и намного менее сложно, чем большинство других ответов, которые я нашел на эта тема) и расширили его. Я полагаю, что кто-то другой может найти это полезным.

Я еще не полностью тестировал это со ВСЕМИ тегами, но каждый тестируемый работал как шарм. Я также подозреваю, что это не самый быстрый код в мире, но мое собственное тестирование с несколькими тысячами форматированных сообщений в ListView показалось на удивление незаметным. YMMV. Код ниже:

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>

С#

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

}

Ответ 5

То же самое я реализовал с помощью Behavior. Код, указанный ниже:

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 - пространство имён импорта

<UserControl x:Class="MyClass"
         xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
         xmlns:behav="clr-namespace:myAssembly.myNameSapce;assembly=myAssembly"
>

Затем, чтобы использовать поведение как:

    <TextBlock TextWrapping="Wrap">
        <i:Interaction.Behaviors>
            <behav:FormatTextBlock FormattedText="{Binding Path=UIMessage}" />
        </i:Interaction.Behaviors>
    </TextBlock>

Ответ 6

Я добавил гиперссылку и поддержку изображений в решение Vincents:

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

Спасибо Винсент за отличный шаблон, он работает как шарм!

Ответ 7

Эта работа для меня:

XAML:

<phone:PhoneApplicationPage x:Class="MyAPP.Views.Class"
                        xmlns:utils="clr-namespace:MyAPP.Utils">

и ваш 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 });

            }
        }));
}

ИСПОЛЬЗУЙТЕ в привязке строки:

String Text = Text <b>Bold</b>;