Universal apps need to have a responsive UI that adapts to hugely differing form factors. This article describes how to build a Border control that hides its content when it’s too large to be fully displayed on the screen. It is implemented as a behavior, since –unlike in WPF- all native WinRT and Windows Phone controls are sealed. Originally, I had built a HideOnTrimTextBlock: a TextBlock that displays nothing if the text becomes too wide to display. The calculation was triggered when the TextBlock’s size changed. I soon discovered two things:
- the SizeChanged event for a TextBlock is often swallowed by its parent, so you have to walk up the visual tree to hook event handlers there, and
- the code used only members of FrameworkElement, so it was applicable to more than just TextBlock controls.
So I turned the HideOnTrimTextBlock into a ShowAllOrNothingBorder.
To detect whether the content of an element is too wide to be displayed, we compare its DesiredSize (don’t forget to Measure first) with its rendered ActualWidth.
I implemented this test as an extension method:
public static class FrameworkElementExtensions
{
/// <summary>
/// Returns whether or not the content of the element is too wide to be displayed entirely.
/// </summary>
public static bool IsContentTooWide(this FrameworkElement element)
{
element.Measure(new Size(double.MaxValue, double.MaxValue));
return element.DesiredSize.Width > (element.ActualWidth + 1);
}
}
I only check the Width here, feel free to bring the Height into the equation.
The ShowAllOrNothing Universal Behavior implements IBehavior and applies to any Border. In the SizeChanged we verify whether the content is too wide or not, and adjust the Opacity of the border's Child (alternatively you could play on the Visibility). Here’s the whole behavior:
/// <summary>
/// A behavior that makes a Border's content disappear when it doesn't entirely fit the screen.
/// </summary>
public class ShowAllOrNothingBehavior : DependencyObject, IBehavior
{
private Border border;
public DependencyObject AssociatedObject
{
get { return this.border; }
}
public void Attach(DependencyObject associatedObject)
{
if (associatedObject is Border)
{
this.border = associatedObject as Border;
this.border.SizeChanged += this.Border_SizeChanged;
}
else
{
throw new Exception("ShowAllOrNothingBehavior Behavior only applies to Border.");
};
}
private void Border_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (this.border.IsContentTooWide())
{
this.border.Child.Opacity = 0;
}
else
{
this.border.Child.Opacity = 1;
}
}
public void Detach()
{
if (this.border != null)
{
this.border.SizeChanged -= this.Border_SizeChanged;
}
}
}
Here’s how to attach the behavior in XAML:
<Page ...
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:behaviors="using:U2UC.WinUni.Behaviors"
...>
<Border>
<interactivity:Interaction.Behaviors>
<behaviors:ShowAllOrNothingBehavior />
</interactivity:Interaction.Behaviors>
<TextBlock Text="I'm sorry, Dave. I'm afraid I can't do that." />
</Border>
Here’s a screen shot of the attached sample app. It illustrates a number of text block responsiveness options, such as word wrapping, character trimming, word trimming, and shrinking. The ShowAllOrNothing behavior is attached to the last one:
The slider at the bottom of the screen determines the width of the text blocks’ parent. When you slide it to the left you’ll see the last text immediately disappear when it’s touched by the red line:
In some use cases it makes sense to entirely hide a control if it doesn’t fit the screen. In the following screen shot, I believe that none of the text blocks actually produce any useful output:
Since it’s a Universal app, it’s also supposed to work on the phone:
All the code is sitting in the Shared project of a Universal App solution. The Windows 8.1 and Windows Phone 8.1 apps just need to reference their own platform-specific Behaviors SDK:
You may want to implement this behavior in a Portable Class Library instead. In that case I suggest you first read this article by Joost van Schaik.
For the sake of completeness, here’s how this code would look like in WPF. Since the control classes are not sealed, we can put everything in a Border subclass:
// Wpf Control.
namespace WpfApplication
{
using System.Windows;
using System.Windows.Controls;
/// <summary>
/// A Border that makes its content disappear when it doesn't entirely fit the screen.
/// </summary>
public class ShowAllOrNothingBorder : Border
{
/// <summary>
/// Initializes a new instance of the <see cref="ShowAllOrNothingBorder"/> class.
/// </summary>
public ShowAllOrNothingBorder()
{
this.SizeChanged += ShowAllOrNothingBorder_SizeChanged;
}
/// <summary>
/// Determines whether content is too wide.
/// </summary>
public bool IsContentTooWide()
{
this.Measure(new Size(double.MaxValue, double.MaxValue));
return this.DesiredSize.Width > (this.ActualWidth + 1);
}
private void ShowAllOrNothingBorder_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (IsContentTooWide())
{
this.Child.Opacity = 0;
}
else
{
this.Child.Opacity = 1;
}
}
}
}
Here’s the full source code, it was written in Visual Studio 2013 Update 2: U2UC.WinUni.Behavior.zip (132.2KB)
Enjoy!
XAML Brewer