Visibility.Hidden for UWP XAML apps

Visibility.Hidden for UWP XAML apps

I recently read the feedback of a .Net developer wanting to migrate his/her WPF application to UWP. One of his/her concerns was that unlike WPF, the UIElement.Visibility property in UWP has only two values: Visibility and Collapsed, missing the third value offered by WPF: Hidden.

What’s the difference between Hidden and Collapsed?

Visibility.Hidden hides the control but keep the space it occupies in the layout. So it renders whitespace instead of the control.

Visibilty.Collapsed doesn’t render the control and doesn’t reserve the whitespace. The space the control would take is ‘collapsed’, hence the name.

Equivalent to Opacity==0?

Not really. If you look for a solution on MSDN forums or Stackoverflow, you will read that you can use Opacity==0 instead. That is only partly true. Visually, it will be similar, but if we think about UI interactions, it’s not.

If you set the opacity of your control to 0, the control will not be visible and the space used kept… but users will still be able to interact with your control, even if not visible, including: touch events, mouse events, Gamepad navigation, Tab navigation, etc…

For example, users will still be able to click on your buttons, scroll your listviews, your controls will still be able to catch the focus (but the focus rectangle will not be visible cause opacity==0, it will provide a very bad user experience).

So to cut a long story short, to create an equivalent of Visibility.Hidden, we need to:

  • Set Opacity to 0.
  • Disable user interactions with IsHitTestVisible.
  • Disable Focus of the UIElement as well as its children!
  • Move the focus if it sets on the current item or a descendant.

How to implement it?

First, we create an enum, equivalent to the WPF one.

public enum TriStateVisibility { Visible = 0, Hidden = 1, Collapsed = 2 };

Then we create an attached property, to have access to the new Visibility property from XAML, code-behind and visual states:

public class TriStateVisibilityExtension : DependencyObject
 {
   public static TriStateVisibility GetVisibility(DependencyObject obj)
   {
     return (TriStateVisibility)obj.GetValue(VisibilityProperty);
   }

   public static void SetVisibility(DependencyObject obj, TriStateVisibility value)
   {
     obj.SetValue(VisibilityProperty, value);
   }

   public static readonly DependencyProperty VisibilityProperty =
      DependencyProperty.RegisterAttached("Visibility", typeof(TriStateVisibility), typeof(TriStateVisibilityExtension),
      new PropertyMetadata(TriStateVisibility.Visible, OnVisibilityChanged));

   private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
   {
     ...
   }
}

So you can write:

  <StackPanel huyn:TriStateVisibilityExtension.Visibility="Hidden" />          

Then we implement OnVisibilityChanged to modify the Visibility and IsHitTestVisible property:

private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  var uiElement = d as UIElement;
  if (uiElement == null)
     return;

  switch(e.OldValue)
  {
     case TriStateVisibility.Hidden:
        uiElement.Opacity = 1;
        uiElement.IsHitTestVisible = true;
        break;
  }

  switch (e.NewValue)
  {
     case TriStateVisibility.Visible:
        uiElement.Visibility = Visibility.Visible;
        break;
     case TriStateVisibility.Hidden:
        uiElement.Visibility = Visibility.Visible;
        uiElement.Opacity = 0;
        uiElement.IsHitTestVisible = false;
        break;
     case TriStateVisibility.Collapsed:
        uiElement.Visibility = Visibility.Collapsed;
        break;
  }

}

This code works, but it’s not enough, we need to make the item (and descendants) not focusable. There is no direct way to do that, so we will use a trick. If we detect that the item is focused, we will directly focus the next item. But it’s not enough, we need also to manage scenarios when user type Shift+Tab or use gamepads, in these scenarios, the new item to focus can be the previous, or the one on top, etc…

So find the correct item to focus we will need to know the focus direction. The only way to do it is via the event GettingFocus (added with Windows 10 Creators Update). We will need a second attached property to save the focus direction in order to pass it to the GotFocus event.

#region FocusLastDirection

protected static FocusNavigationDirection GetFocusLastDirection(DependencyObject obj)
{
   return (FocusNavigationDirection)obj.GetValue(FocusLastDirectionProperty);
}

protected static void SetFocusLastDirection(DependencyObject obj, FocusNavigationDirection value)
{
   obj.SetValue(FocusLastDirectionProperty, value);
}

protected static readonly DependencyProperty FocusLastDirectionProperty =
   DependencyProperty.RegisterAttached("FocusLastDirection", typeof(FocusNavigationDirection), typeof(TriStateVisibilityExtension),
   new PropertyMetadata(FocusNavigationDirection.Next, null));

#endregion

...

private static void UiElement_GettingFocus(UIElement sender, Windows.UI.Xaml.Input.GettingFocusEventArgs args)
{
   args.Handled = true;
   SetFocusLastDirection(sender, args.Direction);
   sender.GotFocus += Sender_GotFocus;
}

private static void Sender_GotFocus(object sender, RoutedEventArgs e)
{
   ((UIElement)sender).GotFocus -= Sender_GotFocus;
   var direction = GetFocusLastDirection((UIElement)sender);
   if (direction == FocusNavigationDirection.None)
   direction = FocusNavigationDirection.Next;
   FocusManager.TryMoveFocus(direction);
}

With the previous code, we made the item (and descendants) not focusable, but what if the item was already focused? We need to manage this scenario too:

private static void CheckIfSelfOrDescendantsNotFocused(UIElement uiElement)
{
   var focusedItem = FocusManager.GetFocusedElement() as UIElement;
   if (focusedItem == null)
      return;
   if(focusedItem == uiElement)
   {
      FocusManager.TryMoveFocus(FocusNavigationDirection.Next);
      return;
   }
   var item = VisualTreeHelper.GetParent(focusedItem);
   while (item != null)
   {
      if (item == uiElement)
      {
         var lastItemFocusable = FocusManager.FindLastFocusableElement(uiElement) as Control;
         if(lastItemFocusable != null)
         {
             SetFocusLastDirection(uiElement, FocusNavigationDirection.Next);
             lastItemFocusable.Focus(FocusState.Programmatic);
         }
         return;
      }

      item = VisualTreeHelper.GetParent(item);
   }
}

And we are done!

Full code + sample

You can download the following sample app:

Sample app

And the full code here:

using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;

namespace Huyn
{
    public enum TriStateVisibility { Visible = 0, Hidden = 1, Collapsed = 2 };
    public class TriStateVisibilityExtension : DependencyObject
    {

        #region FocusLastDirection

        protected static FocusNavigationDirection GetFocusLastDirection(DependencyObject obj)
        {
            return (FocusNavigationDirection)obj.GetValue(FocusLastDirectionProperty);
        }

        protected static void SetFocusLastDirection(DependencyObject obj, FocusNavigationDirection value)
        {
            obj.SetValue(FocusLastDirectionProperty, value);
        }

        protected static readonly DependencyProperty FocusLastDirectionProperty =
            DependencyProperty.RegisterAttached("FocusLastDirection", typeof(FocusNavigationDirection), typeof(TriStateVisibilityExtension),
                new PropertyMetadata(FocusNavigationDirection.Next, null));

        #endregion


        #region Visibility

        public static TriStateVisibility GetVisibility(DependencyObject obj)
        {
            return (TriStateVisibility)obj.GetValue(VisibilityProperty);
        }

        public static void SetVisibility(DependencyObject obj, TriStateVisibility value)
        {
            obj.SetValue(VisibilityProperty, value);
        }

        public static readonly DependencyProperty VisibilityProperty =
            DependencyProperty.RegisterAttached("Visibility", typeof(TriStateVisibility), typeof(TriStateVisibilityExtension),
                new PropertyMetadata(TriStateVisibility.Visible, OnVisibilityChanged));

        private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var uiElement = d as UIElement;
            if (uiElement == null)
                return;

            switch(e.OldValue)
            {
                case TriStateVisibility.Hidden:
                    uiElement.GettingFocus -= UiElement_GettingFocus;
                    uiElement.Opacity = 1;
                    uiElement.IsHitTestVisible = true;
                    break;
            }

            switch (e.NewValue)
            {
                case TriStateVisibility.Visible:
                    uiElement.Visibility = Visibility.Visible;
                    break;
                case TriStateVisibility.Hidden:
                    // Visibility.Hidden doesn't exist in UWP XAML, the equivalent is to: Change opacity to 0, 
                    // disable touch/mouse events and disable focus on children
                    uiElement.Visibility = Visibility.Visible;
                    uiElement.Opacity = 0;
                    uiElement.IsHitTestVisible = false;
                    var control = uiElement as Control;
                    if (control != null)
                        control.Focus(FocusState.Unfocused);

                    uiElement.GettingFocus += UiElement_GettingFocus;
                    CheckIfSelfOrDescendantsNotFocused(uiElement);
                    break;
                case TriStateVisibility.Collapsed:
                    uiElement.Visibility = Visibility.Collapsed;
                    break;
            }

        }

        //If the focus is owned by the current item or by a descendant, move it before hiding the item.
        private static void CheckIfSelfOrDescendantsNotFocused(UIElement uiElement)
        {
            var focusedItem = FocusManager.GetFocusedElement() as UIElement;
            if (focusedItem == null)
                return;
            if(focusedItem == uiElement)
            {
                FocusManager.TryMoveFocus(FocusNavigationDirection.Next);
                return;
            }
            var item = VisualTreeHelper.GetParent(focusedItem);
            while (item != null)
            {
                if (item == uiElement)
                {
                   
                        var lastItemFocusable = FocusManager.FindLastFocusableElement(uiElement) as Control;
                        if(lastItemFocusable != null)
                    {
                        SetFocusLastDirection(uiElement, FocusNavigationDirection.Next);
                        lastItemFocusable.Focus(FocusState.Programmatic);
                    }
                    return;
               }
               
                item = VisualTreeHelper.GetParent(item);
            }
        }
        
        private static void UiElement_GettingFocus(UIElement sender, Windows.UI.Xaml.Input.GettingFocusEventArgs args)
        {
            args.Handled = true;
            SetFocusLastDirection(sender, args.Direction);
            sender.GotFocus += Sender_GotFocus;
        }

        private static void Sender_GotFocus(object sender, RoutedEventArgs e)
        {
            ((UIElement)sender).GotFocus -= Sender_GotFocus;
            var direction = GetFocusLastDirection((UIElement)sender);
            if (direction == FocusNavigationDirection.None)
                direction = FocusNavigationDirection.Next;
            FocusManager.TryMoveFocus(direction);
        }

        #endregion
    }
}
Comments are closed.