How do I Access Buttons inside a UserControl from xaml?

后端 未结 6 564
梦如初夏
梦如初夏 2021-01-17 11:51

At work I have several pages, each with buttons in the same places, and with the same properties. Each page also has minor differences. To that end, we created a userControl

相关标签:
6条回答
  • 2021-01-17 12:20

    You could register a Dependency Property Button on your UserControland handle the initialization in its PropertyChangedCallback.

    Template.xaml.cs

    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Collections.Generic;
    using System.Windows.Markup.Primitives;
    
    namespace TemplateCode
    {
        public partial class Template : UserControl
        {
            public Template()
            {
                InitializeComponent();
            }
    
            public static readonly DependencyProperty ButtonProperty =
                DependencyProperty.Register("Button", typeof(Button), typeof(Template),
                    new UIPropertyMetadata(new PropertyChangedCallback(ButtonChangedCallback)));
    
            public Button Button
            {
                get { return (Button)GetValue(ButtonProperty); }
                set { SetValue(ButtonProperty, value); }
            }
    
            public static List<DependencyProperty> GetDependencyProperties(Object element)
            {
                List<DependencyProperty> properties = new List<DependencyProperty>();
                MarkupObject markupObject = MarkupWriter.GetMarkupObjectFor(element);
                if (markupObject != null)
                {
                    foreach (MarkupProperty mp in markupObject.Properties)
                    {
                        if (mp.DependencyProperty != null)
                        {
                            properties.Add(mp.DependencyProperty);
                        }
                    }
                }
                return properties;
            }
    
            private static void ButtonChangedCallback(object sender, DependencyPropertyChangedEventArgs args)
            {
                // Get button defined by user in MainWindow
                Button userButton     = (Button)args.NewValue;
                // Get template button in UserControl
                UserControl template  = (UserControl)sender;
                Button templateButton = (Button)template.FindName("button");
                // Get userButton props and change templateButton accordingly
                List<DependencyProperty> properties = GetDependencyProperties(userButton);
                foreach(DependencyProperty property in properties)
                {
                    if (templateButton.GetValue(property) != userButton.GetValue(property))
                    {
                        templateButton.SetValue(property, userButton.GetValue(property));
                    }
                }
            }
        }
    }
    

    Template.xaml

    UserControl DataContext is inherited from parent, no need not to set it explicitly

    <UserControl x:Class="TemplateCode.Template"
         x:Name="TemplatePage"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         mc:Ignorable="d"
         d:DesignHeight="350"
         d:DesignWidth="525"
         Background="DarkGray">
    
        <Grid>
            <Button x:Name="button" Width="200" Height="100" Content="Template Button"/>
        </Grid>
    </UserControl>
    

    MainWindow.xaml

    You were setting Button.Content instead of Button

    <Window x:Class="TemplateCode.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        mc:Ignorable="d"
    
        xmlns:templateCode="clr-namespace:TemplateCode"
    
        Title="MainWindow"
        Height="350"
        Width="525">
        <Window.Resources>
            <Button x:Key="UserButton" 
                    Background="Yellow" 
                    Content="Actual Button"
                    Width="200" 
                    Height="100"
                    />
        </Window.Resources>
        <Grid>
            <templateCode:Template Button="{StaticResource UserButton}"/>
        </Grid>
    </Window>
    

    EDIT - Binding Button.Content

    3 ways to do this:

    1. Dependency Properties

    By far the best method. Creating UserControl DP's for every property on the Button is certainly overkill, but for those you want bound to the ViewModel / MainWindow DataContext it makes sense.

    Adding in Template.xaml.cs

    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register("Text", typeof(string), typeof(Template));
    
    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }
    

    Template.xaml

    <UserControl x:Class="TemplateCode.Template"
    
         ...
    
         DataContext="{Binding RelativeSource={RelativeSource Self}}">
        <Grid>
            <Button x:Name="button" Width="200" Height="100" Content="{Binding Text}"/>
        </Grid>
    </UserControl>
    

    MainWindow.xaml

    <Window.Resources>
        <Button x:Key="UserButton" 
                Background="Yellow" 
                Width="200" 
                Height="100"
                />
    </Window.Resources>
    <Grid>
        <templateCode:Template 
            Button="{StaticResource UserButton}" 
            Text="{Binding DataContext.Txt, 
                           RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
    
    </Grid>
    

    Or

    <Window.Resources>
        <Button x:Key="UserButton" 
                Background="Yellow" 
                Content="Actual Button"
                Width="200" 
                Height="100"
                />
    </Window.Resources>
    <Grid>
        <templateCode:Template 
            Button="{StaticResource UserButton}"/>
    </Grid>
    

    Value precedence: UserButton Content > DP Text, so setting the Content in Resources wins.

    2. Creating the Button in your ViewModel

    MVVM purists won't like this, but you could use the Binding mark up instead of StaticResource.

    MainWindow.xaml

    <Grid>
        <templateCode:Template 
            Button="{Binding DataContext.UserButton, 
                             RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
    </Grid>
    

    3. Setting the binding in code

    As you already noticed, a ViewModel prop (e.g. Txt) can't be referenced in Resources because of the order everything is initialized. You can still do it in code later, but it gets a bit messy with the error to prove.

    System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Window', AncestorLevel='1''. BindingExpression:Path=DataContext.Txt; DataItem=null; target element is 'Button' (Name=''); target property is 'Content' (type 'Object')

    Note you need to define the full path on the Content property (setting DataContext on parent won't do).

    MainWindow.xaml

    <Window.Resources>
        <Button x:Key="UserButton" 
                Background="Yellow" 
                Width="200" 
                Height="100"
                Content="{Binding DataContext.Txt, 
                                  RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                />
    </Window.Resources>
    <Grid>
        <templateCode:Template Button="{StaticResource UserButton}"/>
    </Grid>
    

    Template.xaml.cs

    private static void ButtonChangedCallback(object sender, DependencyPropertyChangedEventArgs args)
    {
        // Get button defined by user in MainWindow
        Button userButton = (Button)args.NewValue;
        // Get template button in UserControl
        UserControl template = (UserControl)sender;
        Button templateButton = (Button)template.FindName("button");
        // Get userButton props and change templateButton accordingly
        List<DependencyProperty> properties = GetDependencyProperties(userButton);
        foreach (DependencyProperty property in properties)
        {
        if (templateButton.GetValue(property) != userButton.GetValue(property))
            templateButton.SetValue(property, userButton.GetValue(property));
        }
        // Set Content binding
        BindingExpression bindingExpression = userButton.GetBindingExpression(Button.ContentProperty);
        if (bindingExpression != null)
            templateButton.SetBinding(Button.ContentProperty, bindingExpression.ParentBinding);
    }
    
    0 讨论(0)
  • 2021-01-17 12:22

    Another Option based on @Funk's answer is to make a content control instead of a button on the template, then bind the content control's content to your ButtonProperty in the code behind:

    on the template:

    <ContentControl Content={Binding myButton} Width="200" Height="100"/>
    

    in the template code behind:

    public static readonly DependencyProperty myButtonProperty =
            DependencyProperty.Register("Button", typeof(Button), typeof(Template),
                new UIPropertyMetadata(new PropertyChangedCallback(ButtonChangedCallback)));
    

    and then on the Main Window:

    <Window.Resources>
        <Button x:Key="UserButton" 
                Background="Yellow" 
                Content="Actual Button"
                />
    </Window.Resources>
    <Grid>
        <templateCode:Template myButton="{StaticResource UserButton}"/>
    </Grid>
    

    The nice thing about this is that Visual Studio is smart enough to show this code at design time, as well as having less code overall.

    You can set things constant things (like location, font, and coloring) for your button either on the content control or in a default style, and then modify just the parts you need for you button.

    0 讨论(0)
  • 2021-01-17 12:25

    One option is to simply start writing C# on the xaml page using < ![CDATA[ *** ]]>

    In the Main Window.xaml you change to:

    <templateCode:Template x:Name="test">
        <x:Code><![CDATA[
            Void OnStartup()
            {
                test.MyButton.Content="Actual Button";
                test.MyButton.Background = new SolidColorBrush(Color.FromArgb(255,255,255,0));
            }
            ]]>
        </x:Code>
    

    Then right after Initialize Object() you call OnStartup().

    Although this does let you edit specific properties in the xaml, this is about the same as just writing the code in the code behind, where others expect it to be.

    0 讨论(0)
  • 2021-01-17 12:26

    If you can group your changes to your buttons to one or multiple properties on your datacontext, you could work with DataTriggers:

    <Button x:Name="TestButton">
        <Button.Style>
            <Style>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding IsButtonEnabled}" Value="True">
                        <Setter TargetName="TestButton" Property="Background" Value="Red" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Button.Style>
    </Button>
    

    You can even use multiple conditions with MultiDataTriggers.

    0 讨论(0)
  • 2021-01-17 12:28

    rather than use many dependency properties, prefer style approach. Style contains every property available for a Button control.

    I would create a DependencyProperty for each button style in UserControl.

    public partial class TemplateUserControl : UserControl
    {
        public TemplateUserControl()
        {
            InitializeComponent();
        }
    
        public static readonly DependencyProperty FirstButtonStyleProperty = 
            DependencyProperty.Register("FirstButtonStyle", typeof (Style), typeof (TemplateUserControl));
    
        public Style FirstButtonStyle
        {
            get { return (Style)GetValue(FirstButtonStyleProperty); }
            set { SetValue(FirstButtonStyleProperty, value); }
        }
    
        public static readonly DependencyProperty SecondButtonStyleProperty =
            DependencyProperty.Register("SecondButtonStyle", typeof (Style), typeof (TemplateUserControl));
    
        public Style SecondButtonStyle
        {
            get { return (Style)GetValue(SecondButtonStyleProperty); }
            set { SetValue(SecondButtonStyleProperty, value); }
        }
    }
    

    and then modify xaml for buttons to pick these styles:

    <UserControl x:Class="MyApp.TemplateUserControl"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                 mc:Ignorable="d" 
                 d:DesignHeight="200" d:DesignWidth="300"
                 Background="DarkGray">
        <StackPanel>
            <Button x:Name="_button" Width="200" Height="100" 
                    Style="{Binding Path=FirstButtonStyle, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
            <Button x:Name="_button2" Width="200" Height="100"
                    Style="{Binding Path=SecondButtonStyle, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
        </StackPanel>
    </UserControl>
    

    now when buttons have to be customized, that can achieved by custom styles:

    <StackPanel>
        <StackPanel.Resources>
            <!--common theme properties-->
            <Style TargetType="Button" x:Key="TemplateButtonBase">
                <Setter Property="FontSize" Value="18"/>
                <Setter Property="Foreground" Value="Blue"/>
            </Style>
    
            <!--unique settings of the 1st button-->
            <!--uses common base style-->
            <Style TargetType="Button" x:Key="BFirst" BasedOn="{StaticResource TemplateButtonBase}">
                <Setter Property="Content" Value="1st"/>
            </Style>
    
            <Style TargetType="Button" x:Key="BSecond" BasedOn="{StaticResource TemplateButtonBase}">
                <Setter Property="Content" Value="2nd"/>
            </Style>
        </StackPanel.Resources>
    
        <myApp:TemplateUserControl FirstButtonStyle="{StaticResource BFirst}" 
                                   SecondButtonStyle="{StaticResource BSecond}"/>
    </StackPanel>
    

    0 讨论(0)
  • 2021-01-17 12:34

    The main problem is that Template components are initialized before mainwindow components.I mean all properties of the button in mainwindow are set after the button in template class is initialized. Therefore, as you said value sets to null. All I want to say is about sequence of initializing objects.If you make a trick such a way as follows ;

    public partial class Template : UserControl
    {
        private Button _btn ;
    
        public Template()
        {
    
        }
    
        public Button MyButton
        {
            get
            {
                return _button;
            }
            set
            {
                _btn = value;
                _button = value;
            }
        }
        protected override void OnInitialized(EventArgs e)
        {
            InitializeComponent();
            base.OnInitialized(e);
    
            this._button.Content = _btn.Content;
            this._button.Background = _btn.Background;
            this.Width = _btn.Width;
            this.Height = _btn.Height;
        }
    }
    

    It is going to work undoubtly.

    0 讨论(0)
提交回复
热议问题