Xamarin.Controls – Creating your own Markdown TextBlock in UWP

After talking about using the MarkdownTextView I created and how to accomplish rendering Markdown in iOS and Android without a WebView, I received a few requests to do it for UWP as well.

Check these previous posts out:

  1. Xamarin.Controls – MarkdownTextView
  2. Xamarin.Controls – Creating Your Own Android Markdown TextView
  3. Xamarin.Controls – Creating Your Own iOS Markdown UILabel

Let’s dive in to rendering markdown into a TextBlock in UWP. We’ll break it down into a few steps:

  1. Parse a markdown string into an html string
  2. Create a Behavior for the TextBlock
  3. Parse the html into relevant Span tags in the behavior
  4. Use the new Behavior in our XAML or C#

 

Parsing Markdown

This is traditionally the most difficult part. However, our community is awesome and open sourced a Markdown processor with an MIT license (so use it freely!).

I won’t put the actual code in here because it is overwhelmingly long, but here is a link to it:

https://github.com/SuavePirate/MarkdownTextView/blob/master/src/Forms/SPControls.MarkdownTextView/SPControls.MarkdownTextView/Markdown.cs

Note that this is portable, so you can use it in a PCL without a problem and share it between your platforms.

Now that we have our means of processing the Markdown, let’s create some extension methods to make it easier to parse and do some extra processing like cleaning up our tags, line breaks, etc.

#region MARKDOWN STYLES
private const string ORIGINAL_PATTERN_BEGIN = "<code>";
private const string ORIGINAL_PATTERN_END = "</code>";
private const string PARSED_PATTERN_BEGIN = "<font color=\"#888888\" face=\"monospace\"><tt>";
private const string PARSED_PATTERN_END = "</tt></font>";

#endregion

public static string ToHtml(this string markdownText)
{
    var markdownOptions = new MarkdownOptions
    {
        AutoHyperlink = true,
        AutoNewlines = false,
        EncodeProblemUrlCharacters = false,
        LinkEmails = true,
        StrictBoldItalic = true
    };
    var markdown = new Markdown(markdownOptions);
    var htmlContent = markdown.Transform(markdownText);
    var regex = new Regex("\n");
    htmlContent = regex.Replace(htmlContent, "
");

    var html = htmlContent.HtmlWrapped();
    var regex2 = new Regex("\r");
    html = regex.Replace(html, string.Empty);
    html = regex2.Replace(html, string.Empty);
    return html;
}

///
<summary>
/// Wrap html with a full html tag
/// </summary>
/// <param name="html"></param>
/// <returns></returns>
public static string HtmlWrapped(this string html)
{
    if (!html.StartsWith("<html>") || !html.EndsWith("</html>"))
    {
        html = $"<html><body>{html}</body></html>";
    }
    return html;
}

///<summary>
/// Parses html with code or pre tags and gives them proper
/// styled spans so that Android can parse it properly
/// </summary>
/// <param name="htmlText">The html string</param>
/// <returns>The html string with parsed code tags</returns>
public static string ParseCodeTags(this string htmlText)
{
    if (htmlText.IndexOf(ORIGINAL_PATTERN_BEGIN) < 0) return htmlText;
    var regex = new Regex(ORIGINAL_PATTERN_BEGIN);
    var regex2 = new Regex(ORIGINAL_PATTERN_END);

    htmlText = regex.Replace(htmlText, PARSED_PATTERN_BEGIN);
    htmlText = regex2.Replace(htmlText, PARSED_PATTERN_END);
    htmlText = htmlText.TrimLines();
    return htmlText;
}

public static bool EqualsIgnoreCase(this string text, string text2)
{
    return text.Equals(text2, StringComparison.CurrentCultureIgnoreCase);
}

public static string ReplaceBreaks(this string html)
{
    var regex = new Regex("
");
    html = regex.Replace(html, "\n");
    return html;
}

public static string ReplaceBreaksWithSpace(this string html)
{
    var regex = new Regex("
");
    html = regex.Replace(html, " ");
    return html;
}

public static string TrimLines(this string originalString)
{
    originalString = originalString.Trim('\n');
    return originalString;
}

Now we can properly parse markdown to html:

var markdown = "# Hello *World*";
var html = markdown.ToHtml();
// html = "<h1>Hello <strong>World</strong></h1>"

Create a Behavior

I’m going to take some inspiration from Shawn Kendrot and his post here.

The first thing Shawn does is implement a base Behavior class, so let’s go ahead and use that here:

    // WinRT Implementation of the base Behavior classes
    public abstract class Behavior<T> : Behavior where T : DependencyObject
    {
        protected T AssociatedObject
        {
            get { return base.AssociatedObject as T; }
        }

        protected override void OnAttached()
        {
            base.OnAttached();
            if (this.AssociatedObject == null) throw new InvalidOperationException("AssociatedObject is not of the right type");
        }
    }

    public abstract class Behavior : DependencyObject, IBehavior
    {
        public void Attach(DependencyObject associatedObject)
        {
            AssociatedObject = associatedObject;
            OnAttached();
        }

        public void Detach()
        {
            OnDetaching();
        }

        protected virtual void OnAttached()
        {

        }

        protected virtual void OnDetaching()
        {

        }

        protected DependencyObject AssociatedObject { get; set; }

        DependencyObject IBehavior.AssociatedObject
        {
            get { return this.AssociatedObject; }
        }
    }

Now we can create our actual Behavior implementation to handle the html parsing. If you want the entire file, you can find it in a gist here. Or keep reading so we can break it down.

HtmlTextBehavior.cs

public class HtmlTextBehavior : Behavior<TextBlock>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.Loaded += OnAssociatedObjectLoaded;
        AssociatedObject.LayoutUpdated += OnAssociatedObjectLayoutUpdated;
    }
 
    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.Loaded -= OnAssociatedObjectLoaded;
        AssociatedObject.LayoutUpdated -= OnAssociatedObjectLayoutUpdated;
    }
 
    private void OnAssociatedObjectLayoutUpdated(object sender, object o)
    {
        UpdateText();
    }
 
    private void OnAssociatedObjectLoaded(object sender, RoutedEventArgs routedEventArgs)
    {
        UpdateText();
        AssociatedObject.Loaded -= OnAssociatedObjectLoaded;
    }
 
    private void UpdateText()
    {
        if (AssociatedObject == null) return;
        if (string.IsNullOrEmpty(AssociatedObject.Text)) return;

        string text = AssociatedObject.Text;

        // Just incase we are not given text with elements.
        string modifiedText = string.Format("<div>{0}</div>", text);

        // reset the text because we will add to it.
        AssociatedObject.Inlines.Clear();
        try
        {
            var element = XElement.Parse(modifiedText);
            ParseText(element, AssociatedObject.Inlines);
        }
        catch (Exception)
        {
            // if anything goes wrong just show the html
            AssociatedObject.Text = text;
        }
        AssociatedObject.LayoutUpdated -= OnAssociatedObjectLayoutUpdated;
        AssociatedObject.Loaded -= OnAssociatedObjectLoaded;
    }
    
    /// <summary>
    /// Traverses the XElement and adds text to the InlineCollection.
    /// </summary>
    /// <param name="element"></param>
    /// <param name="inlines"></param>
    private static void ParseText(XElement element, InlineCollection inlines)
    {
        if (element == null) return;

        InlineCollection currentInlines = inlines;
        var elementName = element.Name.ToString().ToUpper();
        switch (elementName)
        {
            case ElementA:
                var link = new Hyperlink();
                var href = element.Attribute("href");
                if(href != null)
                {
                    try
                    {
                        link.NavigateUri = new Uri(href.Value);
                    }
                    catch (System.FormatException) { /* href is not valid */ }
                }
                inlines.Add(link);
                currentInlines = link.Inlines;
                break;
            case ElementB:
            case ElementStrong:
                var bold = new Bold();
                inlines.Add(bold);
                currentInlines = bold.Inlines;
                break;
            case ElementI:
            case ElementEm:
                var italic = new Italic();
                inlines.Add(italic);
                currentInlines = italic.Inlines;
                break;
            case ElementU:
                var underline = new Underline();
                inlines.Add(underline);
                currentInlines = underline.Inlines;
                break;
            case ElementBr:
                inlines.Add(new LineBreak());
                break;
            case ElementP:
                // Add two line breaks, one for the current text and the second for the gap.
                if (AddLineBreakIfNeeded(inlines))
                {
                    inlines.Add(new LineBreak());
                }

                Span paragraphSpan = new Span();
                inlines.Add(paragraphSpan);
                currentInlines = paragraphSpan.Inlines;
                break;
            // TODO: Add ElementH1 - ElementH6 handlers here. They should behave the same way as ElementP with increased font size.
            case ElementLi:
                inlines.Add(new LineBreak());
                inlines.Add(new Run { Text = " • " });
                break;
            case ElementUl:
            case ElementDiv:
                AddLineBreakIfNeeded(inlines);
                Span divSpan = new Span();
                inlines.Add(divSpan);
                currentInlines = divSpan.Inlines;
                break;
        }
        foreach (var node in element.Nodes())
        {
            XText textElement = node as XText;
            if (textElement != null)
            {
                currentInlines.Add(new Run { Text = textElement.Value });
            }
            else
            {
                ParseText(node as XElement, currentInlines);
            }
        }
        // Add newlines for paragraph tags
        if (elementName == ElementP)
        {
            currentInlines.Add(new LineBreak());
        }
    }
    /// <summary>
    /// Check if the InlineCollection contains a LineBreak as the last item.
    /// </summary>
    /// <param name="inlines"></param>
    /// <returns></returns>
    private static bool AddLineBreakIfNeeded(InlineCollection inlines)
    {
        if (inlines.Count > 0)
        {
            var lastInline = inlines[inlines.Count - 1];
            while ((lastInline is Span))
            {
                var span = (Span)lastInline;
                if (span.Inlines.Count > 0)
                {
                    lastInline = span.Inlines[span.Inlines.Count - 1];
                }
            }
            if (!(lastInline is LineBreak))
            {
                inlines.Add(new LineBreak());
                return true;
            }
        }
        return false;
    }
}

That’s a lot of stuff to look at. Let’s break it down. The core of the processing is in the UpdateText and ParseText methods.

Essentially what we are doing here is some layout management, and then parsing the html (parsed into an XElement). It’s different from the approaches we took in Android and iOS where there are native APIs that can parse HTML automatically. But if you looked at the Android post I made before, you’ll remember the extra TagHandler we created that took unsupported html element types and parsed them into Spans for proper formatting. What we did in that situation is a simplified version of what we accomplish here. We take relevant html tags, and create Spans, Bolds, HyperLinks and other relevant types in order to be able to render it within our final TextBlock.

There are a few element types in the parsing that are not included, but can be pretty easily added. For example, code, pre, h1h6, etc. However, these can be easily added to the switch statement! Most of these are going to be handled the same way that ElementP does. So you can add them, and adjust the FontSize or FontFamily of your Span!

Applying the Behavior

Now that we have our HtmlTextBehavior built out, we can apply it to our actual TextBlock!

In XAML:

<TextBlock Text="{Binding MyHtmlText}" FontSize="20" TextWrapping="Wrap">
    <Interactivity:Interaction.Behaviors>
        <local:HtmlTextBehavior />
    </Interactivity:Interaction.Behaviors>
</TextBlock>

Now we can render out MyHtmlText property from our ViewModel directly in our TextBlock!

Advertisements

Xamarin.Tips – Create a Bindable Picker in Xamarin.Forms

EDIT: I forgot to mention, but there was a Pull Request approved and added to the Xamarin.Forms repository that is currently available in Xamarin.Forms v2.3.4-pre1 and should release with the next major update. https://developer.xamarin.com/releases/xamarin-forms/xamarin-forms-2.3/2.3.4-pre1/

It’s unfortunate that Xamarin.Forms’ Picker does not ship with any ability to bind the options. To get around this, we can create a pretty simple control: The BindablePicker

public partial class BindablePicker : Picker
{
    public BindablePicker()
    {
    InitializeComponent();
    this.SelectedIndexChanged += OnSelectedIndexChanged;
    }

    public static BindableProperty ItemsSourceProperty = BindableProperty.Create("ItemsSource", typeof(IEnumerable), typeof(BindablePicker), default(IEnumerable), propertyChanged: OnItemsSourceChanged);

    public static BindableProperty SelectedItemProperty = BindableProperty.Create("SelectedItem", typeof(object), typeof(BindablePicker), default(object), propertyChanged: OnSelectedItemChanged);
    public IEnumerable ItemsSource
    {
        get { return (IEnumerable)GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }

    public object SelectedItem
    {
        get { return (object)GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }

    private static void OnItemsSourceChanged(BindableObject bindable, object oldvalue, object newvalue)
    {
        var picker = bindable as BindablePicker;
        picker.Items.Clear();
        if (newvalue != null)
        {

            foreach (var item in (IEnumerable)newvalue)
            {
                picker.Items.Add(item.ToString());
            }
        }

        // TODO: Add more methods for removing items here
    }

    private void OnSelectedIndexChanged(object sender, EventArgs eventArgs)
    {
        if (SelectedIndex &lt; 0 || SelectedIndex &gt; Items.Count - 1)
        {
            SelectedItem = null;
        }
        else
        {
            SelectedItem = Items[SelectedIndex];
        }
    }
    private static void OnSelectedItemChanged(BindableObject bindable, object oldvalue, object newvalue)
    {
        var picker = bindable as BindablePicker;
        if (newvalue != null)
        {
            picker.SelectedIndex = picker.Items.IndexOf(newvalue.ToString());
        }
    }
}

Then you can use the picker in your XAML like so:

<views:BindablePicker ItemsSource="{Binding TypeList}" Title="Type" SelectedItem="{Binding CurrentType}"/>;

intellitect.com also has a great post on how to do something similar to allow for an enum to be bound to the options.

Creating a Circular Image in XAML

Here is a freeby – my preference for creating circular images in XAML:


<Grid x:Name="SingleImageContainer">
    <Ellipse Height="60" Width="60">
        <Ellipse.Fill>
            <ImageBrush ImageSource="{Binding MyImageUri}" AlignmentX="Center" AlignmentY="Center" />
        </Ellipse.Fill>
    </Ellipse>
 </Grid>

circle (sorry for the bad cropping)

Databinding a Windows FlipView with MVVM Light

MVVM Light is a great cross-platform MVVM Framework, and was chosen to be used in this example. However, it isn’t required to get the same results.

FlipViews were a great addition to the WIndows Control family for Windows Phone and Windows Store applications. It’s easily customized, simple to use, and can give your app a user friendly experience for anything from an image gallery, to a news reader. So let’s get to the code.

Let’s first build the model we are going to use for each of our views in our FlipView. I’m using a PCL for my Models and ViewModels and a Windows Phone 8.1 project for the xaml, but it works in the same fashion for Windows Store.

Here is our model:


public class RotatorItem
 {
     public string Title { get; set; }
     public string ImageUri { get; set; }
     public string Subtitle { get; set; }
 }

Now let’s look at a simple ViewModel with MVVM Light to hold our collection of SimpleRotatorItems.

public class LandingRotatorPageViewModel : ViewModelBase
 {
     #region Private Properties
     private ObservableCollection<RotatorItem> _rotatorItems;
     private RotatorItem _activeItem;
     #endregion

     #region Public Properties
     public ObservableCollection<RotatorItem> RotatorItems
     {
         get
         {
             return _rotatorItems;
         }
         set
         {
             Set(() => RotatorItems, ref _rotatorItems, value);
         }
     }
     public RotatorItem ActiveItem
     {
         get
         {
             return _activeItem;
         }
         set
         {
             Set(() => ActiveItem, ref _activeItem, value);
         }
     }

     #endregion
     public LandingRotatorPageViewModel()
     {
         var rotatorItems = new ObservableCollection<RotatorItem>();
         rotatorItems.Add(new RotatorItem { ImageUri = "/Assets/Logo.scale-240.png", Title = "Title 1", Subtitle = "Subtitle 1"});
         rotatorItems.Add(new RotatorItem { ImageUri = "/Assets/Logo.scale-240.png", Title = "Title 2", Subtitle = "Subtitle 2" });
         rotatorItems.Add(new RotatorItem { ImageUri = "/Assets/Logo.scale-240.png", Title = "Title 3", Subtitle = "Subtitle 3" });
         rotatorItems.Add(new RotatorItem { ImageUri = "/Assets/Logo.scale-240.png", Title = "Title 4", Subtitle = "Subtitle 4" });
         RotatorItems = rotatorItems;
         ActiveItem = rotatorItems.First();
     }
 }

Let’s go ahead now and get our markup done for our page with the FlipView:

<FlipView x:Name="SimpleFlipView" ItemsSource="{Binding RotatorItems}" Grid.Row="1" SelectionChanged="FlipView_SelectionChanged">
    <FlipView.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="200" />
                    <RowDefinition Height="120" />
                    <RowDefinition Height="80" />
                </Grid.RowDefinitions>
                <Image Source="{Binding ImageUri}" Height="150" Grid.Row="0" VerticalAlignment="Top" Margin="0,50,0,0"/>
                <TextBlock TextWrapping="Wrap" TextAlignment="Center" Grid.Row="1" Text="{Binding Title}" FontSize="30" HorizontalAlignment="Center" Margin="20"></TextBlock>
                <TextBlock TextWrapping="Wrap" TextAlignment="Center" Grid.Row="2" FontSize="20" HorizontalAlignment="Center" Margin="20">
                    <Run Text="{Binding Subtitle}"></Run>
               </TextBlock>

            </Grid>
       </DataTemplate>
   </FlipView.ItemTemplate>
</FlipView>

Last but not least, let’s make sure our ViewModel is bound to our Page in our page Class:

 public sealed partial class MainPage : Page
 {
     private LandingRotatorPageViewModel _vm;
     public MainPage()
     {
          this.InitializeComponent();

          _vm = new LandingRotatorPageViewModel();
          this.DataContext = _vm;
     }

     /// <summary>
     /// Invoked when this page is about to be displayed in a Frame.
     /// </summary>
     /// <param name="e">Event data that describes how this page was reached.
     /// This parameter is typically used to configure the page.</param>
     protected override void OnNavigatedTo(NavigationEventArgs e)
     {
     }
}

Now that the Page’s DataContext is set to our ViewModel, the binding we placed on the FlipView element ‘ItemSource=”{Binding RotatorItems}”‘ will bind our FlipView’s Items to the items we created in the constructor for our ViewModel, and will use our ItemTemplate to create our items from the fields on the RotatorItems.

Now let’s take a look at our final result:

FlipView Item
First FlipView Item bound to ViewModel
wp_ss_20150502_0002
Second FlipView Item

So there you have it. An easy solution to using the Windows FlipView Control to display content that is bound to a ViewModel.