2

How would one generate this xaml from C# code instead:

Currently:

<TextBlock>
    Click <Hyperlink Command="{Binding MyCommand}">here</Hyperlink> to continue.
</TextBlock>

What I want:

<TextBlock Text="{Binding MyTextWithHyperlink, Mode=OneWay}" />

public string MyTextWithHyperlink
{
    get
    {
        return ""; //???
    }
}


And yes, I have a valid reason to do it this way instead of in xaml. :)

UPDATE: This is why I want to return the String, because IDataError returns a string...

String IDataError.this[String columnName]
{
    get
    {
        if (columnName == "MyProperty")
        {
            if (something1) return ""; //????
            if (something2) return "some other string";
        }

        return null;
    }
}
myermian
  • 31,823
  • 24
  • 123
  • 215
  • I bet it's easier to fix the problem that's causing you to think you have a valid reason to generate these objects in code than it is to generate these objects in code. – Robert Rossney May 10 '11 at 16:56
  • I updated my question to show _why_ I need to create the object and return the string. – myermian May 10 '11 at 17:36
  • Right. What you want is a hyperlink that contains the text of the error message. So create a property that contains the message, and bind the text of the hyperlink to that property. But wait, you say, `Hyperlink` doesn't have a bindable `Text` property. True: see http://stackoverflow.com/questions/140996/how-can-i-set-the-text-of-a-wpf-hyperlink-via-data-binding. – Robert Rossney May 10 '11 at 21:08
  • @Robert: Halfway there... the entire text is not hyperlinked, only part of it is... this definitely requires some additional logic. – myermian May 10 '11 at 21:10

3 Answers3

4

Unfortunately there's no easy way to do that... As far as I know, the best you can do is return a XAML string and use a converter to parse it.

Warning: ugly code ahead...

XAML

<Window.Resources>
    <local:XamlToTextBlockConverter x:Key="xamlConverter" />
</Window.Resources>
<Grid>
    <ContentControl Content="{Binding MyTextWithHyperlink, Converter={StaticResource xamlConverter}}" />
</Grid>

Converter

public class XamlToTextBlockConverter : IValueConverter
{
    #region Implementation of IValueConverter

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string xaml = value as string;
        if (xaml == null)
            return Binding.DoNothing;

        const string textBlockFormat =
            @"<TextBlock xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">{0}</TextBlock>";
        string fullXaml = string.Format(textBlockFormat, xaml);

        return (TextBlock)XamlReader.Parse(fullXaml);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

ViewModel

public string MyTextWithHyperlink
{
    get { return "Click <Hyperlink Command=\"{Binding MyCommand}\">here</Hyperlink> to continue"; }
}

Note the use of a ContentControl rather than a TextBlock: that's because the TextBlock.Text property can only contain plain text, not a formatted document, and the Inlines property cannot be bound because it's not a dependency property (and anyway it's readonly). Instead we manually create a TextBlock in the converter and assign it to the content of the ContentControl.

It's definitely not a very elegant solution, but it works...

Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • Oh dear, that is ugly. I wonder how the XAML is able to inline the hyperlink right into the text property itself? – myermian May 10 '11 at 18:00
  • @myermian, it doesn't. As I said, the Text property can't be used here because it only supports plain text. So the content is actually added to the Inlines property of the TextBlock. – Thomas Levesque May 10 '11 at 19:57
  • What I mean is the actual xaml line (which does work because that's what I currently have) ... This works and the link is clickable: `Click here` – myermian May 10 '11 at 20:47
  • Oh, and I just SNOOPED it and saw that the Text property is set to 'Click ' (it cuts off). Yet... it still works... very strange things. – myermian May 10 '11 at 20:55
  • @myermian, the content of the `` element is not the Text property but the Inlines property, because the ContentProperty attribute on the TextBlock class defines Inlines as the content, not Text – Thomas Levesque May 10 '11 at 21:51
2

So you're trying to dictate the visual tree from your view model? Bad idea.

Instead, why don't you simply set a state property based on the validation and trigger the visual tree based on that? You could do so using a trigger, or using the visual state manager.

Kent Boogaart
  • 175,602
  • 35
  • 392
  • 393
  • So instead of returning the error string, return perhaps an error code and use that to determine which error message to truly show? – myermian May 10 '11 at 17:59
  • @myermian: essentially, yes. Your validation logic could set another property to value (probably enumeration) that is then used to dictate the UI. – Kent Boogaart May 10 '11 at 23:24
  • You're absolutely right, I gave you the checkmark because it is important to iterate that what I was hoping to intend to do would have broken the M-V-VM pattern by exactly what you said: I'm dictating the visual tree from my VM. Instead I went the route of exposing a few more properties and used `System.ComponentModel.DataAnnotation` (Validation) plus a ValidationHelper class to handle all my validations :) – myermian May 11 '11 at 20:52
2

This control works are a replacement for TextBlock. It has bindable MarkupText property that understands same syntax you use when specifying TextBlock content.

Usage:

<local:MarkupTextBlock MarkupText="{Binding MyText}"/>

in C#

public string MyText
{
    get
    {
        return "My <Bold>link</Bold> is <Hyperlink NavigateUri='http://search.msn.com'>MSN</Hyperlink>.";
    }
}

Control source code:

using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Markup;

/// <summary>
/// The markup text block is a replacement for <see cref="TextBlock"/> 
/// that allows to specify markup content dynamically.
/// </summary>
[ContentProperty("MarkupText")]
[Localizability(LocalizationCategory.Text)]
public class MarkupTextBlock : TextBlock
{
    /// <summary>
    /// The markup text property.
    /// </summary>
    public static readonly DependencyProperty MarkupTextProperty = DependencyProperty.Register(
        "MarkupText", 
        typeof( string ), 
        typeof( MarkupTextBlock ), 
        new FrameworkPropertyMetadata(
            string.Empty, 
            FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, 
            OnTextMarkupChanged));

    private const string FlowDocumentPrefix =
        "<FlowDocument xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'><Paragraph><Span>";

    private const string FlowDocumentSuffix = "</Span></Paragraph></FlowDocument>";

    /// <summary>
    /// Initializes a new instance of the <see cref="MarkupTextBlock"/> class.
    /// </summary>
    /// <param name="markupText">
    /// The markup text.
    /// </param>
    public MarkupTextBlock(string markupText)
    {
        MarkupText = markupText;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="MarkupTextBlock"/> class.
    /// </summary>
    public MarkupTextBlock()
    {
    }

    /// <summary>
    /// Gets or sets content of the <see cref="MarkupTextBlock"/>.
    /// </summary>
    [Localizability(LocalizationCategory.Text)]
    public string MarkupText
    {
        get { return Inlines.ToString(); }
        set { SetValue(MarkupTextProperty, value); }
    }

    private static void OnTextMarkupChanged(
        DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
    {
        var markupTextBlock = dependencyObject as MarkupTextBlock;
        if( markupTextBlock != null )
        {
            var flowDocument = new StringBuilder();
            flowDocument.Append(FlowDocumentPrefix);
            flowDocument.Append(dependencyPropertyChangedEventArgs.NewValue);
            flowDocument.Append(FlowDocumentSuffix);

            var document = (FlowDocument) XamlReader.Parse(flowDocument.ToString());
            var paragraph = document.Blocks.FirstBlock as Paragraph;
            if( paragraph != null )
            {
                var inline = paragraph.Inlines.FirstInline;
                if( inline != null )
                {
                    paragraph.Inlines.Remove(inline);
                    markupTextBlock.Inlines.Clear();
                    markupTextBlock.Inlines.Add(inline);
                }
            }
        }
    }
}
AlexK
  • 228
  • 2
  • 7