Material Design Shadows in WPF

This post will demonstrate how to implement Google’s new Material Design principles in WPF. First, we’ll tackle the Cards/Modal shadows, seen below.


Their shadows consist of two layers: a top shadow for depth and a bottom shadow for definition.

Thankfully, it’s possible to achieve a similar effect in WPF by using only one DropShadowEffect on a Border.

Cards-DropShadowEffect-WPF

<Page
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   <Page.Resources>
      <DropShadowEffect x:Key="z-depth1" BlurRadius="5" ShadowDepth="1" Direction="270" Color="#CCCCCC"/>
      <DropShadowEffect x:Key="z-depth2" BlurRadius="8" ShadowDepth="2.5" Direction="270" Color="#BBBBBB"/>
      <DropShadowEffect x:Key="z-depth3" BlurRadius="14" ShadowDepth="4.5" Direction="270" Color="#BBBBBB"/>
      <DropShadowEffect x:Key="z-depth4" BlurRadius="25" ShadowDepth="8" Direction="270" Color="#BBBBBB"/>
      <DropShadowEffect x:Key="z-depth5" BlurRadius="35" ShadowDepth="13" Direction="270" Color="#BBBBBB"/>
   </Page.Resources>
   <Grid Background="#EEEEEE">
      <StackPanel Orientation="Horizontal">
         <Border Effect="{StaticResource z-depth1}" Width="100" Height="100" Background="White" Margin="25" />
         <Border Effect="{StaticResource z-depth2}" Width="100" Height="100" Background="White" Margin="25" />
         <Border Effect="{StaticResource z-depth3}" Width="100" Height="100" Background="White" Margin="25" />
         <Border Effect="{StaticResource z-depth4}" Width="100" Height="100" Background="White" Margin="25" />
         <Border Effect="{StaticResource z-depth5}" Width="100" Height="100" Background="White" Margin="25" />
      </StackPanel>
   </Grid>
</Page>

To use with a Left/Right Nav or Bottom Toolbar, simply change the Direction of the DropShadowEffect to the appropriate angle (right 0, left 180, top 90, bottom 270).

If you’re interested in reproducing their shadows using two DropShadowEffects, use two controls stacked on an empty Grid or Canvas and apply the effect on both..

Detect URLs and add Hyperlinks to a WPF RichTextBox automatically

This post will demonstrate how to search strings in a WPF RichTextBox for URLs and replace each of them with a Hyperlink.

I’ve been using this code as a replacement for the Forms.RichTextBox DetectURLs property as the WPF RichTextBox does not have it.

If you’re using the RichTextbox as input, there is a solution available on prajakta’s MSDN blog to add hyperlinks OnKeyDown.

Checking if a String is a valid URL

The first goal is to correctly identify which strings in the RichTextBox are considered URLs. We’ll be using a combination of the Uri.IsWellFormedUriString method for Absolute URIs as well as a Regex for Relative URIs.

The following URIs were used as constraints during the design.

Absolute: http://www.google.com
Relative: www.google.com, google.com
Network: \\Marc\Public\
File: C:/file.png

In order to detect the Absolute URIs, we’re using .NET’s Uri.IsWellFormedUriString. Then we’re using the Regex to detect the Relative URIs, and then manually checking whether the URIs as IsUNC and IsFile if both the previous checks failed. IsUNC checks to see if it’s a network path and IsFile checks to see if it’s a local file path. The Regex below will catch both the www.google.com and google.com links successfully.

If for some reason this code catches some URIs that are indesirable, then simply add conditions to deflect them.

private static readonly Regex UrlRegex = new Regex(@"(?#Protocol)(?:(?:ht|f)tp(?:s?)\:\/\/|~/|/)?(?#Username:Password)(?:\w+:\w+@)?(?#Subdomains)(?:(?:[-\w]+\.)+(?#TopLevel Domains)(?:com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|museum|travel|[a-z]{2}))(?#Port)(?::[\d]{1,5})?(?#Directories)(?:(?:(?:/(?:[-\w~!$+|.,=]|%[a-f\d]{2})+)+|/)+|\?|#)?(?#Query)(?:(?:\?(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)(?:&amp;(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)*)*(?#Anchor)(?:#(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)?");

public static bool IsHyperlink(string word)
{
   // First check to make sure the word has at least one of the characters we need to make a hyperlink
   if (word.IndexOfAny(@":.\/".ToCharArray()) != -1)
   {
      if (Uri.IsWellFormedUriString(word, UriKind.Absolute))
      {
         // The string is an Absolute URI
         return true;
      }
      else if (UrlRegex.IsMatch(word))
      {
         Uri uri = new Uri(word, UriKind.RelativeOrAbsolute);

         if (!uri.IsAbsoluteUri
         {
            // rebuild it it with http to turn it into an Absolute URI
            uri = new Uri(@"http://" + word, UriKind.Absolute);
         }

         if (uri.IsAbsoluteUri)
         {
            return true;
         }
      }
      else
      {
         Uri wordUri = new Uri(word);

         // Check to see if URL is a network path
         if (wordUri.IsUnc || wordUri.IsFile)
         {
            return true;
         }
      }
   }

   return false;
}

Iterating through the paragraph and creating Hyperlinks

Now that we have the logic to check to see if a string is a valid URL, we can use it while iterating through our RichTextBox Paragraphs.

public static void DetectURLs(Paragraph par)
{
   string paragraphText = new TextRange(par.ContentStart, par.ContentEnd).Text;

   // Split the paragraph by words
   foreach (string word in paragraphText.Split(' ').ToList())
   {
      if (Hyperlinks.IsHyperlink(word))
      {
         Uri uri = new Uri(word, UriKind.RelativeOrAbsolute);

         if (!uri.IsAbsoluteUri)
         {
            // Prepend it with http
            uri = new Uri(@"http://" + word, UriKind.Absolute);
         }

         if (uri != null)
         {
            TextPointer position = par.ContentStart;

            // Find the word in the paragraph
            while (position != null)
            {
               if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text)
               {
                  string textRun = position.GetTextInRun(LogicalDirection.Forward);

                  // Find the starting index of any substring that matches "word".
                  int indexInRun = textRun.IndexOf(word);
                  if (indexInRun != 0)
                  {
                     TextPointer start = position.GetPositionAtOffset(indexInRun);
                     TextPointer end = start.GetPositionAtOffset(word.Length);
                     var link = new Hyperlink(start, end)
                     {
                        NavigateUri = uri,

                     };
                     link.Click += Hyperlink_Click;
                  }
               }

               position = position.GetNextContextPosition(LogicalDirection.Forward);
            }
         }
      }
   }
}

public static void Hyperlink_Click(object sender, EventArgs e)
{
   Process.Start((sender as Hyperlink).NavigateUri.AbsoluteUri);
}

DetectURLs was designed to accept a Paragraph, however you could always modify it to accept whatever would suit your design. The important part is to get a TextPointer for both the start and the end of the string you wish to become a Hyperlink.

Usage example

Finally, use our new DetectURLs along with a Paragraph.

Paragraph paragraph = new Paragraph();
paragraph.Inlines.Add(new Run("www.google.com google.com http://www.google.com"));

DetectURLs(paragraph);

richTextBox.Document.Blocks.Add(paragraph);