Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Holding event fires on untouched items in ListView

I have a UserControl with a Grid that is subscribed to a Holding event. The problem is that the Holding event fires for the item I targeted as well as some other items in the ListView. I'm using the control as a DataTemplate, by the way.

<ListView ItemsSource="{Binding ...}" Margin="0, 0, 0, 0">
    ...
    <ListView.ItemTemplate>
        <DataTemplate>
            <local:MyUserControl/>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

User control code-behind:

    private bool isDescriptionVisible = false;

    private void Grid_Holding(object sender, HoldingRoutedEventArgs e)
    {
        if (!isDescriptionVisible)
        {
            DescriptionFadeIn.Begin();
            isDescriptionVisible = true;
        }
    }

    private void Grid_Tapped(object sender, TappedRoutedEventArgs e)
    {
        if (isDescriptionVisible)
        {
            DescriptionFadeOut.Begin();
            isDescriptionVisible = false;
        }
    }

User Control contents:

<Grid.Resources>
    <Storyboard x:Name="FadeIn">
            <DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="DescriptionLayer"
                             Duration="0:0:0.3" To=".8"/>
        </Storyboard>

        <Storyboard x:Name="FadeOut">
            <DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="DescriptionLayer"
                             Duration="0:0:0.3" To="0"/>
        </Storyboard>
</Grid.Resources>

<Grid Margin="0, 0, 0, 48" Holding="Grid_Holding" Tapped="Grid_Tapped">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="1*"/>
    </Grid.RowDefinitions>
    <Image Source="{Binding Img}" Stretch="UniformToFill" Height="240" Width="450"/>
    <Grid x:Name="DescriptionLayer" Background="Black" Opacity="0">
        <TextBlock Text="{Binding Description}" FontSize="16" TextWrapping="Wrap" Margin="0, 9, 0, 0" MaxHeight="170" TextTrimming="CharacterEllipsis"/>
    </Grid>
    <StackPanel Grid.Row="1" Margin="12">
        <TextBlock Text="{Binding Author}" FontSize="16"/>
        <TextBlock Text="{Binding Title}" FontSize="18"/>
    </StackPanel>
</Grid>

I was unable to use storyboards on items contained in a DataTemplate, so that forced me to use move its contents to a UserControl.

Does this issue have to do with virtualization? How can I fix this?

Alternatives will do.

UPDATE:

Some SO posts suggested that the recycling mode caused items to be reused. I've added VirtualizingStackPanel.VirtualizationMode="Standard" to my ListView but the problem surprisingly persists.

So now I need to figure out a way to prevent other items from repeating the same opacity value (which is not databound because it is set via a storyboard).

UPDATE 2:

Now the Description that fades in upon holding the displayed item completely disappears when it goes out of view:

<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <VirtualizingStackPanel/>
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
like image 773
pcnThird Avatar asked Oct 20 '22 04:10

pcnThird


2 Answers

I found a workaround; I've added a property called DescriptionLayerOpacity to my model and set it to a default value of 0 when I'm adding a new item to my ObservableCollection in my viewmodel.

I've also added 2-way binding to change the source property (DescriptionLayerOpacity) so the view gets updated with the changes made by the storyboards:

<Grid ... Opacity="{Binding DescriptionLayerOpacity, Mode=TwoWay}">
    <TextBlock .../>
</Grid>

In a nutshell, all the UserControl's data had to be databound to avoid being repeated in the other items in the ListView.

This really isn't an elegant solution and it hasn't been fully tested. Until I find a real solution, this will suffice.

Update: Not fully working

I recently discovered that some items don't respond when other items are selected. To get those items to respond, I have to tap prior to the tap+hold event (both are mutually exclusive events, by the way).

Update 2:

Everything seems to be working fine after removing the if statements in the codebehind. But binding to an opacity property in the model to get it to work is still a smelly solution.

like image 107
pcnThird Avatar answered Oct 31 '22 14:10

pcnThird


I like how you are trying to do here. For touch screen devices, we don't have MouseOver event so this is one of the ways when you want to show some extra info without messing up the UI.

However, a much cleaner solution would be, to create a reusable Behavior that encapsulates all the UI logic & animations.

To do this, you will first need to include the reference Behaviors SDK (XAML).

The Behavior implementation is straight forward. The AssociatedObject would be the Grid that receives the touch events and then you need to create a DependencyProperty TextBlockName to retrieve the description TextBlock instance. Once you have the instance, you just need to write the animations for it in C#.

public class ShowHideDescriptionBehavior : DependencyObject, IBehavior
{
    public string TextBlockName
    {
        get { return (string)GetValue(TextBlockNameProperty); }
        set { SetValue(TextBlockNameProperty, value); }
    }

    // Using a DependencyProperty as the backing store for TextBlockName.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty TextBlockNameProperty =
        DependencyProperty.Register("TextBlockName", typeof(string), typeof(ShowHideDescriptionBehavior), new PropertyMetadata(string.Empty));

    public DependencyObject AssociatedObject { get; set; }

    public void Attach(DependencyObject associatedObject)
    {
        this.AssociatedObject = associatedObject;
        var panel = (Panel)this.AssociatedObject;

        panel.Holding += AssociatedObject_Holding;
        panel.Tapped += AssociatedObject_Tapped;
    }

    private void AssociatedObject_Tapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
    {
        var animation = new DoubleAnimation
        {
            Duration = TimeSpan.FromMilliseconds(300),
            To = 0
        };
        Storyboard.SetTarget(animation, this.DescriptionTextBlock);
        Storyboard.SetTargetProperty(animation, "Opacity");

        var storyboard = new Storyboard();
        storyboard.Children.Add(animation);
        storyboard.Begin();
    }

    private void AssociatedObject_Holding(object sender, Windows.UI.Xaml.Input.HoldingRoutedEventArgs e)
    {
        var animation = new DoubleAnimation
        {
            Duration = TimeSpan.FromMilliseconds(300),
            To = 0.8
        };
        Storyboard.SetTarget(animation, this.DescriptionTextBlock);
        Storyboard.SetTargetProperty(animation, "Opacity");

        var storyboard = new Storyboard();
        storyboard.Children.Add(animation);
        storyboard.Begin();
    }

    private TextBlock _descriptionTextBlock;
    private TextBlock DescriptionTextBlock
    {
        get
        {
            if (_descriptionTextBlock == null)
            {
                var panel = (Panel)this.AssociatedObject;
                // todo: add validation
                _descriptionTextBlock = (TextBlock)panel.FindName(this.TextBlockName);
            }

            return _descriptionTextBlock;
        }
    }

    public void Detach()
    {
        var panel = (Panel)this.AssociatedObject;

        panel.Holding -= AssociatedObject_Holding;
        panel.Tapped -= AssociatedObject_Tapped;
    }
}

This Behavior can then be attached to your DataTemplate's top level Grid. Note that you don't need a UserControl to wrap it anymore.

Also, make sure you have a background color for the Grid so it can receive touch events. The last thing I did was to change the ListView's ItemContainerStyle's HorizontalContentAlignment to Stretch so the Grid gets stretched all the way across to the right.

<Page.Resources>
    <DataTemplate x:Key="GroupTemplate">
        <Grid Background="Transparent" Margin="12">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Interactivity:Interaction.Behaviors>
                <local:ShowHideDescriptionBehavior TextBlockName="Description" />
            </Interactivity:Interaction.Behaviors>
            <Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}" Width="110" Height="110">
                <Image Source="{Binding Img}" Height="110" Width="110"/>
            </Border>
            <StackPanel Grid.Column="1" Margin="10,0,0,0">
                <TextBlock Text="{Binding Author}" Style="{StaticResource TitleTextBlockStyle}"/>
                <TextBlock x:Name="Description" Opacity="0" Text="{Binding Description}" Style="{StaticResource CaptionTextBlockStyle}" TextWrapping="NoWrap"/>
            </StackPanel>
        </Grid>
    </DataTemplate>
</Page.Resources>

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" DataContext="{Binding Source={StaticResource SampleDataSource}}">
    <ListView ItemTemplate="{StaticResource GroupTemplate}" ItemsSource="{Binding Groups}">
        <ListView.ItemContainerStyle>
            <Style TargetType="ListViewItem">
                <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
            </Style>
        </ListView.ItemContainerStyle>
    </ListView>
</Grid>
like image 44
Justin XL Avatar answered Oct 31 '22 14:10

Justin XL