Como criar um UserControl WPF com conteúdo NAMED

100

Eu tenho um conjunto de controles com comandos e lógica anexados que são constantemente reutilizados da mesma maneira. Decidi criar um controle de usuário que contenha todos os controles e lógicas comuns.

No entanto, também preciso do controle para poder conter o conteúdo que pode ser nomeado. Tentei o seguinte:

<UserControl.ContentTemplate>
    <DataTemplate>
        <Button>a reused button</Button>
        <ContentPresenter Content="{TemplateBinding Content}"/>
        <Button>a reused button</Button>
    </DataTemplate>
</UserControl.ContentTemplate>

No entanto, parece que qualquer conteúdo colocado dentro do controle do usuário não pode ser nomeado. Por exemplo, se eu usar o controle da seguinte maneira:

<lib:UserControl1>
     <Button Name="buttonName">content</Button>
</lib:UserControl1>

Recebo o seguinte erro:

Não é possível definir o valor do atributo Name 'buttonName' no elemento 'Button'. 'Botão' está no escopo do elemento 'UserControl1', que já tinha um nome registrado quando foi definido em outro escopo.

Se eu remover o buttonName, ele será compilado, mas preciso nomear o conteúdo. Como posso conseguir isso?

Ryan
fonte
Isso é uma coincidência. Eu estava prestes a fazer esta pergunta! Eu tenho o mesmo problema. Fatorar o padrão comum da IU em um UserControl, mas querendo se referir à IU do conteúdo pelo nome.
mackenir de
3
Esse cara encontrou uma solução envolvendo livrar-se do arquivo XAML de seu controle personalizado e construir a interface do usuário do controle personalizado de forma programática. Esta postagem do blog tem mais a dizer sobre o assunto.
mackenir de
2
Por que você não usa a maneira ResourceDictionary? Defina o DataTemplate nele. Ou use a palavra-chave BasedOn para herdar o controle. Apenas alguns caminhos que eu seguiria antes de fazer a IU code-behind no WPF ...
Louis Kottmann

Respostas:

46

A resposta é não usar um UserControl para fazer isso.

Crie uma classe que estenda ContentControl

public class MyFunkyControl : ContentControl
{
    public static readonly DependencyProperty HeadingProperty =
        DependencyProperty.Register("Heading", typeof(string),
        typeof(HeadingContainer), new PropertyMetadata(HeadingChanged));

    private static void HeadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((HeadingContainer) d).Heading = e.NewValue as string;
    }

    public string Heading { get; set; }
}

então use um estilo para especificar o conteúdo

<Style TargetType="control:MyFunkyControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="control:MyFunkyContainer">
                <Grid>
                    <ContentControl Content="{TemplateBinding Content}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

e finalmente - use-o

<control:MyFunkyControl Heading="Some heading!">            
    <Label Name="WithAName">Some cool content</Label>
</control:MyFunkyControl>
Sybrand
fonte
2
Eu descobri que esta é realmente a solução mais confortável, já que você pode desenvolver o ControlTemplate em um UserControl comum usando o designer e que o transforma em um estilo com modelo de controle associado.
Oliver Weichhold
5
Hmm, é um pouco estranho que funcione para você, porque eu tentei aplicar essa abordagem e, no meu caso, ainda recebo esse erro infame.
greenoldman
8
@greenoldman @Badiboy Acho que sei por que não funcionou para você. você provavelmente acabou de alterar um código existente de UserControlpara herdar ContentControl. Para resolver, basta adicionar uma nova classe ( não XAML com CS). E então (espero) funcionará. se quiser, criei uma pequena solução VS2010
itsho
1
Muitos anos depois, e gostaria de poder votar a favor novamente :)
Drew Noakes de
2
Eu acho HeadingContainere MyFunkyContaineré para ser MyFunkyControl?!
Martin Schneider
20

Parece que isso não é possível quando o XAML é usado. Os controles personalizados parecem ser um exagero quando, na verdade, tenho todos os controles de que preciso, mas só preciso agrupá-los com um pouco de lógica e permitir conteúdo nomeado.

A solução no blog de JD, como sugere mackenir, parece ter o melhor compromisso. Uma maneira de estender a solução JD para permitir que os controles ainda sejam definidos em XAML pode ser a seguinte:

    protected override void OnInitialized(EventArgs e)
    {
        base.OnInitialized(e);

        var grid = new Grid();
        var content = new ContentPresenter
                          {
                              Content = Content
                          };

        var userControl = new UserControlDefinedInXAML();
        userControl.aStackPanel.Children.Add(content);

        grid.Children.Add(userControl);
        Content = grid;           
    }

No meu exemplo acima, criei um controle de usuário denominado UserControlDefinedInXAML, que é definido como qualquer controle de usuário normal usando XAML. Em meu UserControlDefinedInXAML, tenho um StackPanel chamado aStackPanel no qual desejo que meu conteúdo nomeado apareça.

Ryan
fonte
Tenho descoberto que tenho problemas de vinculação de dados ao usar esse mecanismo de re-parentalidade do conteúdo. A vinculação de dados parece configurada corretamente, mas o preenchimento inicial dos controles da fonte de dados não funciona corretamente. Acho que o problema está limitado a controles que não são filhos diretos do apresentador de conteúdo.
mackenir
Não tive a chance de experimentar isso desde então, mas não tive nenhum problema de ligação de dados aos controles definidos no UserControlDefinedInXAML (do exemplo acima) ou aos controles adicionados ao ContentPresenter até agora. Eu tenho vinculado a dados apenas por meio de código (não XAML - não tenho certeza se isso faz diferença).
Ryan
Parece que faz a diferença. Acabei de tentar usar XAML para minhas ligações de dados no caso que você descreveu e não funcionou. Mas se eu definir em código, funciona!
Ryan
3

Outra alternativa que usei é apenas definir a Namepropriedade no Loadedevento.

No meu caso, eu tinha um controle bastante complexo que não queria criar no code-behind, e ele procurou um controle opcional com um nome específico para determinado comportamento, e desde que percebi que poderia definir o nome em um DataTemplateAchei que poderia fazer isso no Loadedevento também.

private void Button_Loaded(object sender, RoutedEventArgs e)
{
    Button b = sender as Button;
    b.Name = "buttonName";
}
Rachel
fonte
2
Se você fizer isso, as ligações usando o nome não funcionarão ... a menos que você defina as ligações no code-behind.
Torre
3

Às vezes, você só precisa fazer referência ao elemento do C #. Dependendo do caso de uso, você pode definir um em x:Uidvez de um x:Namee acessar os elementos chamando um método localizador de Uid como Get object por seu Uid no WPF .

Dani
fonte
1

Você pode usar este auxiliar para definir o nome dentro do controle do usuário:

using System;
using System.Reflection;
using System.Windows;
using System.Windows.Media;
namespace UI.Helpers
{
    public class UserControlNameHelper
    {
        public static string GetName(DependencyObject d)
        {
            return (string)d.GetValue(UserControlNameHelper.NameProperty);
        }

        public static void SetName(DependencyObject d, string val)
        {
            d.SetValue(UserControlNameHelper.NameProperty, val);
        }

        public static readonly DependencyProperty NameProperty =
            DependencyProperty.RegisterAttached("Name",
                typeof(string),
                typeof(UserControlNameHelper),
                new FrameworkPropertyMetadata("",
                    FrameworkPropertyMetadataOptions.None,
                    (d, e) =>
                    {
                        if (!string.IsNullOrEmpty((string)e.NewValue))
                        {
                            string[] names = e.NewValue.ToString().Split(new char[] { ',' });

                            if (d is FrameworkElement)
                            {
                                ((FrameworkElement)d).Name = names[0];
                                Type t = Type.GetType(names[1]);
                                if (t == null)
                                    return;
                                var parent = FindVisualParent(d, t);
                                if (parent == null)
                                    return;
                                var p = parent.GetType().GetProperty(names[0], BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);
                                p.SetValue(parent, d, null);
                            }
                        }
                    }));

        public static DependencyObject FindVisualParent(DependencyObject child, Type t)
        {
            // get parent item
            DependencyObject parentObject = VisualTreeHelper.GetParent(child);

            // we’ve reached the end of the tree
            if (parentObject == null)
            {
                var p = ((FrameworkElement)child).Parent;
                if (p == null)
                    return null;
                parentObject = p;
            }

            // check if the parent matches the type we’re looking for
            DependencyObject parent = parentObject.GetType() == t ? parentObject : null;
            if (parent != null)
            {
                return parent;
            }
            else
            {
                // use recursion to proceed with next level
                return FindVisualParent(parentObject, t);
            }
        }
    }
}

e sua janela ou código de controle atrás definido, você controla por propriedade:

 public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

    }

    public Button BtnOK { get; set; }
}

sua janela xaml:

    <Window x:Class="user_Control_Name.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:test="clr-namespace:user_Control_Name"
            xmlns:helper="clr-namespace:UI.Helpers" x:Name="mainWindow"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <test:TestUserControl>
                <Button helper:UserControlNameHelper.Name="BtnOK,user_Control_Name.MainWindow"/>
            </test:TestUserControl>
            <TextBlock Text="{Binding ElementName=mainWindow,Path=BtnOK.Name}"/>
        </Grid>
    </Window>

UserControlNameHelper obtém seu nome de controle e seu nome de classe para definir Control como Property.

Ali Yousefi
fonte
0

Decidi criar uma propriedade extra para cada elemento que preciso obter:

    public FrameworkElement First
    {
        get
        {
            if (Controls.Count > 0)
            {
                return Controls[0];
            }
            return null;
        }
    }

Isso me permite acessar os elementos filho em XAML:

<TextBlock Text="{Binding First.SelectedItem, ElementName=Taxcode}"/>
Aliceraunsbaek
fonte
0
<Popup>
    <TextBox Loaded="BlahTextBox_Loaded" />
</Popup>

Código por trás:

public TextBox BlahTextBox { get; set; }
private void BlahTextBox_Loaded(object sender, RoutedEventArgs e)
{
    BlahTextBox = sender as TextBox;
}

A solução real seria a Microsoft corrigir esse problema, bem como todos os outros com árvores visuais quebradas, etc. Hipoteticamente falando.

15ee8f99-57ff-4f92-890c-b56153
fonte
0

Ainda outra solução alternativa: referencie o elemento como RelativeSource .

voccoeisuoi
fonte
0

Eu tive o mesmo problema ao usar um TabControl ao colocar um monte de controles nomeados em.

Minha solução alternativa foi usar um modelo de controle que contém todos os meus controles para serem mostrados em uma página de guia. Dentro do modelo, você pode usar a propriedade Name e também vincular dados às propriedades do controle nomeado de outros controles, pelo menos dentro do mesmo modelo.

Como conteúdo do controle TabItem, use um controle simples e defina o ControlTemplate de acordo:

<Control Template="{StaticResource MyControlTemplate}"/>

Para acessar esses controles nomeados dentro do modelo a partir do código por trás, você precisará usar a árvore visual.

David Wellna
fonte