Как получить доступ к кнопкам внутри UserControl из xaml?

На работе у меня есть несколько страниц, каждая с кнопками в тех же местах и ​​с теми же свойствами. Каждая страница также имеет незначительные отличия. С этой целью мы создали шаблон UserControl и поместили в него все кнопки, а затем применили этот пользовательский элемент управления ко всем страницам. Однако теперь довольно сложно получить доступ к кнопкам и изменить их с каждой страницы xaml, потому что они находятся внутри UserControl на странице..... Как мне элегантно получить доступ к кнопкам с каждой страницы?

Что я пробовал:

  • В настоящее время мы связываемся с связью свойств зависимостей. Мне не нравится этот вариант, потому что у меня много кнопок, и вам нужно управлять множеством свойств на этих кнопках. Результатом являются сотни свойств зависимостей и реальный беспорядок, который нужно пропустить, когда нам нужно что-то изменить.

  • Другой способ - использовать стили. Мне нравится этот метод в целом, но поскольку эти кнопки находятся внутри другого элемента управления, становится сложно их модифицировать, и шаблон будет точно соответствовать только одной кнопке за один раз.

  • Adam Kemp опубликовал о том, чтобы позволить пользователю просто вставить свою собственную кнопку здесь, и это метод я в настоящее время пытается внедрить/изменить. К сожалению, у меня нет доступа к Xamarin.

Хотя шаблон вставляется при запуске кода, шаблон не обновляет кнопку правильно. Если я установил точку останова в MyButton Setter, я вижу, что значение на самом деле является пустой, а не той, которую я назначил в своем главном окне. Как это исправить?

Здесь приведен упрощенный код:

Мой шаблон UserControl xaml:

<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"
     DataContext="{Binding RelativeSource={RelativeSource Self}}"
     Background="DarkGray">

     <Grid>
          <Button x:Name="_button" Width="200" Height="100" Content="Template Button"/>
     </Grid>
</UserControl>

Мой шаблон UserControl Code Behind:

using System.Windows.Controls;

namespace TemplateCode
{
    public partial class Template : UserControl
    {
        public static Button DefaultButton;

        public Template()
        {
             InitializeComponent();
        }

        public Button MyButton
        {
            get
            {
                 return _button;
            }
            set
            {
                _button = value; //I get here, but value is a blank button?!

                // Eventually, I'd like to do something like:
                // Foreach (property in value) 
                // {
                //     If( value.property != DefaultButton.property) )
                //     {
                //         _button.property = value.property;
                //     }
                // }
                // This way users only have to update some of the properties
            }
        }
    }
}

И теперь приложение, в котором я хочу его использовать:

<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"
    Background="LimeGreen"
    DataContext="{Binding RelativeSource={RelativeSource Self}}" >

    <Grid>
        <templateCode:Template>
            <templateCode:Template.MyButton>

                <Button Background="Yellow" 
                    Content="Actual Button"
                    Width="200" 
                    Height="100"/>

            </templateCode:Template.MyButton>
        </templateCode:Template>
    </Grid>
</Window>

И теперь код за:

Using System.Windows;
Namespace TemplateCode
{
    Public partial class MainWindow : Window
    {
        Public MainWindow()
        {
            InitializeComponent();
        }
    }
}

Изменить: Пока я хочу удалить ненужные свойства зависимостей в шаблоне userControl, мне все равно хотелось бы установить привязки свойств кнопки из XAML.

Ответ 1

Другой вариант, основанный на ответе @Funk, заключается в том, чтобы создать элемент управления содержимым вместо кнопки в шаблоне, а затем привязать контент управления контентом к вашему ButtonProperty в коде позади:

в шаблоне:

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

в коде шаблона позади:

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

а затем в главном окне:

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

Приятная вещь в том, что Visual Studio достаточно умен, чтобы показывать этот код во время разработки, а также иметь меньше кода в целом.

Вы можете установить для своей кнопки все элементы (например, местоположение, шрифт и раскраску), либо в элементе управления контентом, либо в стиле по умолчанию, а затем изменить только те части, которые вам нужны для кнопки.

Ответ 2

Вы можете зарегистрировать Свойство зависимостей Button на UserControl и обработать инициализацию в 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 наследуется от родителя, не нужно явно указывать его

<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

Вы устанавливали Button.Content вместо 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 - кнопка привязки. Контент

3 способа сделать это:

1. Свойства зависимостей

На сегодняшний день лучший метод. Создание UserControl DP для каждого свойства в Button, безусловно, избыточно, но для тех, кого вы хотите связать с ViewModel/MainWindow DataContext, это имеет смысл.

Добавление в 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>

или

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

Значения приоритета: UserButton Контент > DP Text, поэтому настройка содержания в Resources побеждает.

2. Создание кнопки в ViewModel

Пуристам MVVM это не понравится, но вы можете использовать знак Binding вместо StaticResource.

MainWindow.xaml

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

3. Установка привязки в коде

Как вы уже заметили, нельзя использовать ссылку ViewModel (например, Txt) в Resources из-за того, что все инициализируется. Вы все еще можете сделать это в коде позже, но он немного запутан с ошибкой, чтобы доказать.

Ошибка System.Windows.Data: 4: не удается найти источник для привязки с ссылка 'RelativeSource FindAncestor, AncestorType = 'System.Windows.Window', AncestorLevel = '1' '. BindingExpression: Path = DataContext.Txt; DataItem = NULL; целевой элемент 'Button' (Name= ''); target - это "Content" (тип "Object" )

Обратите внимание, что вам нужно определить полный путь в свойстве Content (настройка DataContext на родительском не будет).

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);
}

Ответ 3

а не использовать многие свойства зависимостей, предпочитайте стиль. Стиль содержит все свойства, доступные для элемента управления Button.

Я бы создал DependencyProperty для каждого стиля кнопки в 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); }
    }
}

а затем измените xaml для кнопок, чтобы выбрать эти стили:

<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>

теперь, когда кнопки должны быть настроены, что может быть достигнуто с помощью пользовательских стилей:

<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>

введите описание изображения здесь

Ответ 4

Если вы можете сгруппировать свои изменения на своих кнопках с одним или несколькими свойствами в вашем datacontext, вы можете работать с 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>

Вы можете использовать несколько условий с помощью MultiDataTriggers.

Ответ 5

Основная проблема заключается в том, что компоненты шаблона инициализируются перед компонентами mainwindow. Я имею в виду, что все свойства кнопки в mainwindow устанавливаются после инициализации кнопки в классе шаблона. Поэтому, как вы сказали, значение устанавливает значение null. Все, что я хочу сказать, это о последовательности инициализации объектов. Если вы сделаете трюк следующим образом:

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;
    }
}

Это будет работать бесспорно.

Ответ 6

Один из вариантов - просто начать писать С# на странице xaml, используя <! [CDATA [***]] >

В главном окне .xaml вы измените на:

<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>

Затем сразу после инициализации объекта() вы вызываете OnStartup().

Хотя это позволяет вам редактировать определенные свойства в xaml, это примерно то же самое, что просто писать код в коде позади, где другие ожидают его.

Ответ 7

Использовать лямбда-выражение или делегировать методы для установки определенного метода для каждой кнопки с каждой страницей