Search code examples
xamlbuttonuwpcontroltemplateflyout

UWP Derived Button With Flyout: Binding within the Style


General: in a Style for a custom control, is it possible to bind to another DependencyProperty (e.g. MySecondProperty) value from within a <Setter Property="MyFirstProperty">?

For what purpose? To accomplish the following:

1.) Derive some MyButton : Button control, which has an additional List<string> FlyoutSource dependency property on it.

2.) Define a MyButtonStyle, which has a <Setter Property="Flyout"> element defining the Button.Flyout property (since MyButton : Button).

The Flyout will have a ListView in it, whose ItemsSource must bind to MyButton.FlyoutSource

<Style TargetType="local:MyButton" x:Key="MyButtonStyle">
    <Setter Property="Background" Value="Green"/>
    <Setter Property="Flyout">
        <Setter.Value>
            <Flyout>
                <!-- &&&&&&& THE FOLLOWING LINE DOES NOT WORK PROPERLY &&&&&&& -->
                <ListView ItemsSource="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=FlyoutSource}">
                    <ListView.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding}"/>
                        </DataTemplate>
                    </ListView.ItemTemplate>
                </ListView>
            </Flyout>
        </Setter.Value>
    </Setter>
</Style>

How would I like to use the solution:

<local:MyButton 
    FlyoutSource="{x:Bind FlyoutSourceList, Mode=TwoWay}"
    Style="{StaticResource MyButtonStyle}">
</local:MyButton

More Detail: the MyButton class:

public class MyButton : Button
{
    public MyButton()
    {
        this.DefaultStyleKey = typeof(Button);
    }

    public static DependencyProperty FlyoutSourceProperty = DependencyProperty.Register(
    "FlyoutSource", typeof(List<string>), typeof(MyButton),
    new PropertyMetadata(null, new PropertyChangedCallback(OnFlyoutSourceChanged)));

    public List<string> FlyoutSource
    {
        get { return (List<string>)GetValue(FlyoutSourceProperty); }
        set { SetValue(FlyoutSourceProperty, value); }
    }

    public static void OnFlyoutSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        Debug.WriteLine("");
    }
}

Solution

  • You actually don't need to subclass Button to do this, just make FlyoutSource an attached property.

    You can't use RelativeSource mode TemplatedParent here because it is not within a ControlTemplate.

    It seems that the only way for the flyout content to obtain information from it's attached element is through DataContext inheritance. I could only come up with this, but it involves a lot of binding gymnastics. I don't recommend it.

    public class ViewProps
    {
        public static object GetFlyoutListSource(DependencyObject obj)
        {
            return (object)obj.GetValue(FlyoutListSourceProperty);
        }
    
        public static void SetFlyoutListSource(DependencyObject obj, object value)
        {
            obj.SetValue(FlyoutListSourceProperty, value);
        }
    
        public static readonly DependencyProperty FlyoutListSourceProperty =
            DependencyProperty.RegisterAttached("FlyoutListSource", typeof(object), typeof(ViewProps), new PropertyMetadata(null));
    }
    
    <Grid x:Name="MyGrid">
        <Grid.Resources>
            <Style x:Key="FlyoutButton" TargetType="Button">
                <Setter Property="Flyout">
                    <Setter.Value>
                        <Flyout>
                            <ListView ItemsSource="{Binding (local:ViewProps.FlyoutListSource)}"/>
                        </Flyout>
                    </Setter.Value>
                </Setter>
            </Style>
        </Grid.Resources>
    
        <Button
            Style="{StaticResource FlyoutButton}"
            Content="Button"
            DataContext="{Binding RelativeSource={RelativeSource Self}}"
            local:ViewProps.FlyoutListSource="{Binding ElementName=MyGrid, Path=DataContext.ItemsSource}"/>
    </Grid>
    

    If you want to subclass Button, then you can do something like this.

    ListFlyoutButton.cs

    public sealed class ListFlyoutButton : Button
    {
        public object ItemsSource
        {
            get { return (object)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }
    
        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(object), typeof(ListFlyoutButton), new PropertyMetadata(null));
    
        public ListFlyoutButton()
        {
            this.DefaultStyleKey = typeof(ListFlyoutButton);
        }
    
        protected override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
    
            ((FrameworkElement)((Flyout)Flyout).Content).DataContext = this;
        }
    }
    

    Themes\Generic.xaml

    <ResourceDictionary
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="***">
    
        <Style TargetType="local:ListFlyoutButton">
            <Setter Property="Background" Value="{ThemeResource ButtonBackground}" />
            <Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
            <Setter Property="BorderBrush" Value="{ThemeResource ButtonBorderBrush}" />
            <Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
            <Setter Property="Padding" Value="8,4,8,4" />
            <Setter Property="HorizontalAlignment" Value="Left" />
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
            <Setter Property="FontWeight" Value="Normal" />
            <Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
            <Setter Property="UseSystemFocusVisuals" Value="True" />
            <Setter Property="FocusVisualMargin" Value="-3" />
            <Setter Property="Flyout">
                <Setter.Value>
                    <Flyout>
                        <ListView ItemsSource="{Binding ItemsSource}"/>
                    </Flyout>
                </Setter.Value>
            </Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="local:ListFlyoutButton">
                        <Grid x:Name="RootGrid" Background="{TemplateBinding Background}">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal">
                                        <Storyboard>
                                            <PointerUpThemeAnimation Storyboard.TargetName="RootGrid" />
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="PointerOver">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBackgroundPointerOver}" />
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBorderBrushPointerOver}" />
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundPointerOver}" />
                                            </ObjectAnimationUsingKeyFrames>
                                            <PointerUpThemeAnimation Storyboard.TargetName="RootGrid" />
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Pressed">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBackgroundPressed}" />
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBorderBrushPressed}" />
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundPressed}" />
                                            </ObjectAnimationUsingKeyFrames>
                                            <PointerDownThemeAnimation Storyboard.TargetName="RootGrid" />
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBackgroundDisabled}" />
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBorderBrushDisabled}" />
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundDisabled}" />
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <ContentPresenter x:Name="ContentPresenter"
                                    BorderBrush="{TemplateBinding BorderBrush}"
                                    BorderThickness="{TemplateBinding BorderThickness}"
                                    Content="{TemplateBinding Content}"
                                    ContentTransitions="{TemplateBinding ContentTransitions}"
                                    ContentTemplate="{TemplateBinding ContentTemplate}"
                                    Padding="{TemplateBinding Padding}"
                                    HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                                    VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                                    AutomationProperties.AccessibilityView="Raw" />
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ResourceDictionary>
    

    MainPage.xaml

    <local:ListFlyoutButton Content="Button" ItemsSource="{Binding Items}"/>
    

    MainPage.xaml.cs

    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            DataContext = new
            {
                Items = new[] { "Apple", "Banana" },
            };
        }
    }
    

    It would be nice if we didn't have to duplicate the entire default Button style.