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>
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.
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With