5
votes

I'm working on a Windows Store App with C# and ran into something where I can't imagine it's difficult, but I can't find out how to do it.

On my MainPage.xaml I created a user control: A StackPanel with horizontal orientation, with just an Image and a TextBlock inside. Like this:

<!-- language: lang-xml -->
<StackPanel Width="300" Height="100" Orientation="Horizontal" Margin="0,0,0,20" Tapped="LoremIpsum_Tapped">
    <Image Source="/Assets/pic.jpg" Margin="20"/>
    <TextBlock FontFamily="Segoe UI" FontSize="30" VerticalAlignment="Center">
        Lorem Ipsum
    </TextBlock>
</StackPanel>

Which looks like this:

enter image description here

I use it as a custom kind of button to get to some sub-page. Now the thing is, there are no animations. I wanted to have the typical animations that ListItems have in a SplitView: shrink when pressed, grow back to normal when either released or when the pointer exits the virtual borders of the control. I couldn't find the animation declaration/definition associated with those ListItems (Common.StandardStyles.xaml Standard130ItemTemplate). But I found out (here) that there are predefined animations for that: PointerDownThemeAnimation and PointerUpThemeAnimation. But then it took me quite a while to find out how to apply them to the control. You might think the sample mentioned on this site specifically about those pointer theme animations (about C#) should help, but it leads to sample for HTML/Javascript animations. But I found solutions here, here and here.

So I needed Storyboards in the control resources, a name so the control can be targeted and event handlers in the code-behind.

Applied on my XAML it becomes this:

<!-- language: lang-xml -->
<StackPanel x:Name="LoremIpsum" Width="300" Height="100" Orientation="Horizontal" Margin="0,0,0,20" Tapped="LoremIpsum_Tapped" PointerPressed="LoremIpsum_AnimDown" PointerReleased="LoremIpsum_AnimUp" PointerExited="LoremIpsum_AnimUp">
    <Image Source="/Assets/pic.jpg" Margin="20"/>
    <TextBlock FontFamily="Segoe UI" FontSize="30" VerticalAlignment="Center">
        Lorem Ipsum
    </TextBlock>
    <StackPanel.Resources>
        <Storyboard x:Name="pointerDownStoryboard">
            <PointerDownThemeAnimation TargetName="LoremIpsum" />
        </Storyboard>
        <Storyboard x:Name="pointerUpStoryboard">
            <PointerUpThemeAnimation TargetName="LoremIpsum" />
        </Storyboard>
    </StackPanel.Resources>
</StackPanel>

Plus the additional event handlers in the code-behind:

<!-- language: lang-cs -->
private void Latest_AnimDown(object sender, PointerRoutedEventArgs e)
{
    pointerDownStoryboard.Begin();
}
private void Latest_AnimUp(object sender, PointerRoutedEventArgs e)
{
    pointerUpStoryboard.Begin();
}

This worked. But... as I have lots of those kind of user controls, I certainly don't want to add all that for every control. As mentioned before the Standard130ItemTemplate didn't help. So I thought about custom controls. I was hoping I could just define a MyStackPanel that's nothing but a StackPanel + the StoryBoards, and that the targeting to the x:Name would work and maybe I could put the event handlers in the LayoutAwarePage code-behind, so all others inherit from it.

I started looking for how to do that and found this sample of how to create custom controls: XAML user and custom controls sample.

There's the custom control in a Generic.xml:

<!-- language: lang-xml -->
xmlns:local="using:UserAndCustomControls">
<Style TargetType="local:BasicCustomControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:BasicCustomControl">
                <Border
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And a code file, but not code-behind:

<!-- language: lang-cs -->
namespace UserAndCustomControls
{
    public sealed class BasicCustomControl : Control
    {
        public BasicCustomControl()
        {
            this.DefaultStyleKey = typeof(BasicCustomControl);
        }
    }
}

But the sample didn't contain anything about animations. So I played around with the example, trying to add the StoryBoards to the Border or to the ControlTemplate, adding event handlers to a self created Generic.xaml.cs code-behind. Nothing worked.

Then I found this, but wasn't sure about how and why to put an event handler into the BasicCustomControl class. I tried it anyway:

In Generic.xaml:

<!-- language: lang-xml -->
xmlns:local="using:UserAndCustomControls">
<Style TargetType="local:BasicCustomControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:BasicCustomControl">
                <Border x:Name="Border"
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
                    <Border.Resources>
                        <Storyboard x:Name="pointerDownStoryboard">
                            <PointerDownThemeAnimation TargetName="Border" />
                        </Storyboard>
                        <Storyboard x:Name="pointerUpStoryboard">
                            <PointerUpThemeAnimation TargetName="Border" />
                        </Storyboard>
                    </Border.Resources>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And in BasicCustomControl.cs:

<!-- language: lang-cs -->
protected override void OnPointerPressed(PointerRoutedEventArgs e)
{
    ((Storyboard)this.Resources["PointerDownThemeAnimation"]).Begin(this);
}

That didn't work. The Begin() method doesn't take arguments and without the argument the build succeeded, but I got an System.Runtime.InteropServices.COMException when clicking on the control.

Then I found this on SO: How to add XAML storyboard animation to a full blown WPF Custom Control in an XBAP?

This seems to be an interesting solution, but I don't get it. Here's also some description of VisualState stuff for custom controls, but again I don't know how to apply this to my needs: Quickstart: control templates

Now at this point I spent quite some time on this and I'm just thinking that for this simple thing - a simple, even pre-defined animation for a custom control -, there must be a simple solution. I hope I'm just overlooking something and it's not really that complicated.

To sum up the questions:

  1. Where's the animation of the Standard130ItemTemplate defined and "attached" to the template?
  2. Is there a control that I can "inherit" from that behaves the way I want (just those up/down animations on three events)?
  3. Is there a way to create that kind of control, that I can inherit from - with XAML + Code?
  4. Is there a way to do this just with XAML? Is this preferable to the former way?
  5. Is there a simple example or tutorial on this?
  6. Are there any collections of custom controls that I can download and use? I wasn't able to find any.
1

1 Answers

0
votes

Create a custom style. In this example I bound the Image Source to the content field. So I just had to put my path to my image in that field (ex. "Assets/Image.png"). It would be nice if adding custom animations like this was easier to figure out the first time.

    <Style x:Key="ImageButtonStyle" TargetType="ButtonBase">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ButtonBase">
                <Grid x:Name="RootGrid" Background="Transparent">
                    <Image x:Name="ImageLabel" Source="{TemplateBinding Content}"/>
                    <Rectangle
                            x:Name="FocusVisualWhite"
                            IsHitTestVisible="False"
                            Stroke="{StaticResource FocusVisualWhiteStrokeThemeBrush}"
                            StrokeEndLineCap="Square"
                            StrokeDashArray="1,1"
                            Opacity="0"
                            StrokeDashOffset="1.5"/>
                    <Rectangle
                            x:Name="FocusVisualBlack"
                            IsHitTestVisible="False"
                            Stroke="{StaticResource FocusVisualBlackStrokeThemeBrush}"
                            StrokeEndLineCap="Square"
                            StrokeDashArray="1,1"
                            Opacity="0"
                            StrokeDashOffset="0.5"/>

                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal"/>
                            <VisualState x:Name="PointerOver">
                            </VisualState>
                            <VisualState x:Name="Pressed">
                                <Storyboard>
                                    <PointerDownThemeAnimation TargetName="ImageLabel" />
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Disabled">
                            </VisualState>
                        </VisualStateGroup>
                        <VisualStateGroup x:Name="FocusStates">
                            <VisualState x:Name="Focused">
                                <Storyboard>
                                    <DoubleAnimation
                                            Storyboard.TargetName="FocusVisualWhite"
                                            Storyboard.TargetProperty="Opacity"
                                            To="1"
                                            Duration="0"/>
                                    <DoubleAnimation
                                            Storyboard.TargetName="FocusVisualBlack"
                                            Storyboard.TargetProperty="Opacity"
                                            To="1"
                                            Duration="0"/>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Unfocused" />
                            <VisualState x:Name="PointerFocused" />
                        </VisualStateGroup>
                        <VisualStateGroup x:Name="CheckStates">
                            <VisualState x:Name="Checked">
                                <Storyboard>
                                    <DoubleAnimation Duration="0" To="0" Storyboard.TargetName="OutlineGlyph" Storyboard.TargetProperty="Opacity"/>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BackgroundGlyph" Storyboard.TargetProperty="Foreground">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource AppBarItemForegroundThemeBrush}"/>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BackgroundCheckedGlyph" Storyboard.TargetProperty="Visibility">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="Content" Storyboard.TargetProperty="Foreground">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource AppBarItemPressedForegroundThemeBrush}"/>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Unchecked"/>
                            <VisualState x:Name="Indeterminate"/>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>