Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditional rendering of the data template in XAML

Tags:

xaml

uwp

uwp-xaml

I have a list of text blocks, that may include urls inside, smth like:

  • Build failed, see more here: http://...
  • Build succeeded
  • App http://myapp/ cannot be started, see more here: http://...

I need to display this (endless) list in a UWP app. Considering this list can be used in multiple views inside the app, I made it a common template:

<ResourceDictionary>
  <ControlTemplate x:Key="ListItemTemplate" TargetType="ItemsControl">
    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="50"/>
        <ColumnDefinition Width="*"/>
      </Grid.ColumnDefinitions>
      <Image Grid.Column="0" Source="{Binding image_url}"/>
      <TextBlock Grid.Column="1" Text="{Binding status}"/>
    </Grid>
  </ControlTemplate>
</ResourceDictionary>

In this template links are treated like regular text (which is expected). As I understand, to make links work I need to wrap them into <HyperLink> tag, but I cannot do this in template, because I don't know where exactly links will be and how many of them will appear.

Is there any way to implement some renderer method, that could generate item's body (<TextBlock>) in code, processing passed value?

Probably converter could help me, but if I understand correctly, it only accept value from binding, and I need to reference whole instance.

UPD: Expanding solution from the accepted answer:

Resource dictionary:

<ResourceDictionary xmlns:resources="using:NamespaceWithTextBlockExt">
  <ControlTemplate x:Key="ListItemTemplate" TargetType="ItemsControl">
    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="50"/>
        <ColumnDefinition Width="*"/>
      </Grid.ColumnDefinitions>
      <Image Grid.Column="0" Source="{Binding image_url}"/>
      <TextBlock Grid.Column="1" resources:TextBlockExt.XAMLText="{Binding Text}"/>
    </Grid>
  </ControlTemplate>
</ResourceDictionary>

Processor somewhere in your project:

public static class TextBlockExt
{
    public static String GetXAMLText(TextBlock obj)
    {
        return (String)obj.GetValue(XAMLTextProperty);
    }

    public static void SetXAMLText(TextBlock obj, String value)
    {
        obj.SetValue(XAMLTextProperty, value);
    }

    /// <summary>
    /// Convert raw string from ViewModel into formatted text in a TextBlock: 
    /// 
    /// @"This <Bold>is a test <Italic>of the</Italic></Bold> text."
    /// 
    /// Text will be parsed as XAML TextBlock content. 
    /// 
    /// See WPF TextBlock documentation for full formatting. It supports spans and all kinds of things. 
    /// 
    /// </summary>
    public static readonly DependencyProperty XAMLTextProperty =
        DependencyProperty.RegisterAttached("XAMLText", typeof(String), typeof(TextBlockExt),
                                             new PropertyMetadata("", XAMLText_PropertyChanged));

    private static void XAMLText_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is TextBlock)
        {
            var ctl = d as TextBlock;

            try
            {
                //  XAML needs a containing tag with a default namespace. We're parsing 
                //  TextBlock content, so make the parent a TextBlock to keep the schema happy. 
                //  TODO: If you want any content not in the default schema, you're out of luck. 
                var value = e.NewValue;

                var strText = String.Format(@"<TextBlock xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">{0}</TextBlock>", e.NewValue);

                TextBlock parsedContent = Windows.UI.Xaml.Markup.XamlReader.Load(strText) as TextBlock;

                //  The Inlines collection contains the structured XAML content of a TextBlock
                ctl.Inlines.Clear();

                var inlines = parsedContent.Inlines.ToList();
                parsedContent.Inlines.Clear();

                //  UI elements are removed from the source collection when the new parent 
                //  acquires them, so pass in a copy of the collection to iterate over. 
                ctl.Inlines.Concat(inlines);
                inlines.ForEach(x => ctl.Inlines.Add(x));
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(String.Format("Error in Ability.CAPS.WPF.UIExtensions.TextBlock.XAMLText_PropertyChanged: {0}", ex.Message));
                throw;
            }
        }
    }
}

I'm not sure this is the best way, but it works. I just need to preprocess bound value and wrap all URLs into hyperlink tags:

"App <Hyperlink NavigateUri=\"http://app/\">myapp</Hyperlink>"

I assume this should work with any other content, like <InlineUIContainer>

like image 824
snooopcatt Avatar asked Nov 08 '22 03:11

snooopcatt


1 Answers

You can write an attached behavior that parses a string as XAML using XamlReader.Load(Stream), and adds the resulting control to the target control. Here's one I wrote which does it with TextBlock content, which can include Hyperlink. That's for WPF not UWP; there may be some differences.

Yours would have to do a little additional work: It would take a non-XAML string, and before parsing as XAML it would have to find URLs and replace them with XAML Hyperlink elements in the string. Then you'd parse that.

It would be cleaner to put that second part in a value converter. Call it HyperLinksToXAMLConverter:

<TextBlock
    local:XAMLText="{Binding status, Converter={StaticResource HyperLinksToXAML}}"
    />
like image 71
15ee8f99-57ff-4f92-890c-b56153 Avatar answered Dec 04 '22 01:12

15ee8f99-57ff-4f92-890c-b56153