A Windows 10, UWP ListView Control with Swiped Actions?

I came across a forum question which was asking whether someone could provide the starting point for a ListView control which acted in a similar way to the built-in mail app on Windows 10 such that you can use a swipe-left/swipe-right gesture to flag/delete mail on Windows 10.

I wanted to answer the question and I gave it quite a bit of thought in terms of how it might be done and whether it was better to try and;

    1. Come up with a custom ListView which made this possible.
    2. Come up with a custom ListViewItem which made this possible.
    3. Re-template a ListViewItem to make this possible.

and I did explore those routes a little but I didn’t have a lot of success in a short period of time and it was mainly because of the relationship between a ListView and the ListViewItem or because of the slightly hard-wired nature of a ListViewItem’s template.

What I mean here is that if you try and re-template a ListViewItem you can quickly bounce up against it expecting to have the ListViewItemPresenter as the first item in its template which proved a bit limiting for me here.

Rather than take any of these routes, I knocked up an experimental control that I called a SwipeContentControl which I could use like this;

  <Grid
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <local:SwipeContentControl
      VerticalAlignment="Center"
      HorizontalContentAlignment="Stretch"
      Swiped="OnSwiped">
      <local:SwipeContentControl.LeftContent>
        <Grid
          Background="LimeGreen">
          <TextBlock
            Text="Flag"
            Foreground="White"
            Margin="0,0,4,0"
            HorizontalAlignment="Right"
            VerticalAlignment="Center" />
        </Grid>
      </local:SwipeContentControl.LeftContent>
      <local:SwipeContentControl.RightContent>
        <Grid
          Background="Red">
          <TextBlock
            Text="Delete"
            Margin="4,0,0,0"
            Foreground="White"
            HorizontalAlignment="Left"
            VerticalAlignment="Center" />
        </Grid>
      </local:SwipeContentControl.RightContent>
      <Grid
        Background="AliceBlue">
        <TextBlock
          FontSize="18"
          Margin="20"
          HorizontalAlignment="Center"
          Text="This is my content" />
      </Grid>
    </local:SwipeContentControl>
  </Grid>

so the control has this notion of 3 pieces of content;

    • the main content to display
    • the content to display if the main content is swiped left
    • the content to display if the main content is swiped right

and it fires an event called Swiped containing the direction of the swipe if it detects that the user swipes the content significantly to the left or right. Here’s a quick screen capture;

I’m not sure that I did a fully fledged job on the SwipeContentControl here but I wrote a control that derived from Control and added what seemed like the bare bones to it in order to get it built;

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

  // these names line up with visual states in generic.xaml so
  // rename at your peril.
  enum SwipeDirection
  {
    Left,
    Right,
    Default
  };
  class SwipeEventArgs : EventArgs
  {
    public SwipeDirection Direction { get; set; }
  }
  [ContentProperty(Name = "Content")]
  class SwipeContentControl : Control
  {
    public event EventHandler<SwipeEventArgs> Swiped;

    public static DependencyProperty LeftContentProperty =
      DependencyProperty.Register(
        "LeftContent", typeof(object), typeof(SwipeContentControl), null);

    public static DependencyProperty RightContentProperty =
      DependencyProperty.Register(
        "RightContent", typeof(object), typeof(SwipeContentControl), null);

    public static DependencyProperty ContentProperty =
      DependencyProperty.Register(
        "Content", typeof(object), typeof(SwipeContentControl), null);

    public static DependencyProperty LeftContentTemplateProperty =
      DependencyProperty.Register(
        "LeftContentTemplate", typeof(DataTemplate), typeof(SwipeContentControl), null);

    public static DependencyProperty RightContentTemplateProperty =
      DependencyProperty.Register(
        "RightContentTemplate", typeof(DataTemplate), typeof(SwipeContentControl), null);

    public static DependencyProperty ContentTemplateProperty =
      DependencyProperty.Register(
        "ContentTemplate", typeof(DataTemplate), typeof(SwipeContentControl), null);


    public object LeftContent
    {
      get
      {
        return (base.GetValue(LeftContentProperty));
      }
      set
      {
        base.SetValue(LeftContentProperty, value);
      }
    }
    public DataTemplate LeftContentTemplate
    {
      get
      {
        return ((DataTemplate)base.GetValue(LeftContentTemplateProperty));
      }
      set
      {
        base.SetValue(LeftContentTemplateProperty, value);
      }
    }
    public DataTemplate RightContentTemplate
    {
      get
      {
        return ((DataTemplate)base.GetValue(RightContentTemplateProperty));
      }
      set
      {
        base.SetValue(RightContentTemplateProperty, value);
      }
    }
    public DataTemplate ContentTemplate
    {
      get
      {
        return ((DataTemplate)base.GetValue(ContentTemplateProperty));
      }
      set
      {
        base.SetValue(ContentTemplateProperty, value);
      }
    }
    public object RightContent
    {
      get
      {
        return (base.GetValue(RightContentProperty));
      }
      set
      {
        base.SetValue(RightContentProperty, value);
      }
    }
    public object Content
    {
      get
      {
        return (base.GetValue(ContentProperty));
      }
      set
      {
        base.SetValue(ContentProperty, value);
      }
    }
    public SwipeContentControl()
    {
      this.DefaultStyleKey = typeof(SwipeContentControl);
      this.ManipulationMode = ManipulationModes.TranslateX;
      this.ManipulationDelta += OnManipulationDelta;
      this.ManipulationCompleted += OnManipulatedCompleted;
    }
    void OnManipulatedCompleted(object sender,
      ManipulationCompletedRoutedEventArgs e)
    {
      this.TranslateContent(0);

      if (IsManipulationSignificant(e.Cumulative.Translation.X))
      {
        SwipeEventArgs args = new SwipeEventArgs()
        {
          Direction = e.Cumulative.Translation.X < 0 ?
            SwipeDirection.Left : SwipeDirection.Right
        };
        this.Swiped?.Invoke(this, args);
      }
    }
    bool IsManipulationSignificant(double x)
    {
      bool significant = Math.Abs(x) >
        (SIGNIFICANT_TRANSLATE_FACTOR * this.contentElement.ActualWidth);

      return (significant);
    }
    void OnManipulationDelta(object sender,
      ManipulationDeltaRoutedEventArgs e)
    {
      this.SetVisualStateForManipulation(e.Cumulative.Translation);
      this.TranslateContent(e.Cumulative.Translation.X);
    }
    void SetVisualStateForManipulation(Point p)
    {
      var direction = this.DirectionOfManipulation(p);

      VisualStateManager.GoToState(this, direction.ToString(), true);
    }
    void TranslateContent(double x)
    {
      if (this.contentElement != null)
      {
        // This may well break if there's already a transform on this element 😦
        TranslateTransform transform = 
          (this.contentElement.RenderTransform as TranslateTransform);

        if (transform == null)
        {
          transform = new TranslateTransform();
          this.contentElement.RenderTransform = transform;
        }
        transform.X = x;
      }
    }
    SwipeDirection DirectionOfManipulation(Point p)
    {
      SwipeDirection d = SwipeDirection.Default;

      if (p.X != 0)
      {
        d = p.X < 0 ? SwipeDirection.Left : SwipeDirection.Right;
      }
      return (d);
    }
    protected override void OnApplyTemplate()
    {
      base.OnApplyTemplate();
      this.contentElement = this.GetTemplateChild("contentPresenter") as FrameworkElement;
    }
    static readonly double SIGNIFICANT_TRANSLATE_FACTOR = 0.20d;
    FrameworkElement contentElement;
  }

and I tried to come up with a generic.xaml template for it in my project’s Themes folder;

<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:App9">

  <Style
    TargetType="local:SwipeContentControl">
    <Setter
      Property="Template">
      <Setter.Value>
        <ControlTemplate
          TargetType="local:SwipeContentControl">
          <Grid
            Background="{TemplateBinding Background}">
            <VisualStateManager.VisualStateGroups>
              <VisualStateGroup
                x:Name="leftRightStates">
                <VisualState
                  x:Name="Left">
                  <VisualState.Setters>
                    <Setter
                      Target="leftContentPresenter.(UIElement.Visibility)"
                      Value="Visible" />
                  </VisualState.Setters>
                </VisualState>
                <VisualState
                  x:Name="Right">
                  <VisualState.Setters>
                    <Setter
                      Target="rightContentPresenter.(UIElement.Visibility)"
                      Value="Visible" />
                  </VisualState.Setters>
                </VisualState>
                <VisualState
                  x:Name="Default" />
              </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
            <ContentPresenter
              Visibility="Collapsed"
              x:Name="leftContentPresenter"
              Content="{TemplateBinding LeftContent}" />
            <ContentPresenter
              Visibility="Collapsed"
              x:Name="rightContentPresenter"
              Content="{TemplateBinding RightContent}" />
            <ContentPresenter
              x:Name="contentPresenter"
              Foreground="{TemplateBinding Foreground}"
              Content="{TemplateBinding Content}"
              ContentTemplate="{TemplateBinding ContentTemplate}"
              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
              VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

and it seemed to work out reasonably well and then I could make use of it in a ListView;


    <ListView>
      <ListView.ItemContainerStyle>
        <Style
          TargetType="ListViewItem">
          <Setter
            Property="HorizontalContentAlignment"
            Value="Stretch" />
        </Style>
      </ListView.ItemContainerStyle>
      <ListView.Items>
        <x:String>Mail One</x:String>
        <x:String>Mail Two</x:String>
        <x:String>Mail Three</x:String>
      </ListView.Items>
      <ListView.ItemTemplate>
        <DataTemplate>
          <local:SwipeContentControl
            HorizontalContentAlignment="Stretch"
            Swiped="OnSwiped">
            <local:SwipeContentControl.LeftContent>
              <Grid
                Background="LimeGreen">
                <TextBlock
                  Text="Flag"
                  Foreground="White"
                  Margin="0,0,4,0"
                  HorizontalAlignment="Right"
                  VerticalAlignment="Center" />
              </Grid>
            </local:SwipeContentControl.LeftContent>
            <local:SwipeContentControl.RightContent>
              <Grid
                Background="Red">
                <TextBlock
                  Text="Delete"
                  Margin="4,0,0,0"
                  Foreground="White"
                  HorizontalAlignment="Left"
                  VerticalAlignment="Center" />
              </Grid>
            </local:SwipeContentControl.RightContent>
            <Grid
              Background="AliceBlue">
              <TextBlock
                FontSize="18"
                Margin="20"
                HorizontalAlignment="Center"
                Text="{Binding}" />
            </Grid>
          </local:SwipeContentControl>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>

and it’s a long way from perfect but it’s perhaps a starting point for this type of thing that someone might take and develop into something better.

The code for the entire thing is here for download if you want to take it on from here and, remember, it was a 20-minutes of effort attempt rather than a full-on attempt to build a real control.

2 thoughts on “A Windows 10, UWP ListView Control with Swiped Actions?

Comments are closed.