Cómo crear un UserControl de WPF con contenido NAMED

100

Tengo un conjunto de controles con comandos y lógica adjuntos que se reutilizan constantemente de la misma manera. Decidí crear un control de usuario que contenga todos los controles y la lógica comunes.

Sin embargo, también necesito el control para poder contener contenido que pueda ser nombrado. Intenté lo siguiente:

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

Sin embargo, parece que no se puede nombrar ningún contenido colocado dentro del control de usuario. Por ejemplo, si uso el control de la siguiente manera:

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

Recibo el siguiente error:

No se puede establecer el valor del atributo de nombre 'buttonName' en el elemento 'Button'. 'Botón' está bajo el alcance del elemento 'UserControl1', que ya tenía un nombre registrado cuando se definió en otro alcance.

Si elimino el buttonName, se compila, sin embargo, necesito poder nombrar el contenido. ¿Cómo puedo conseguir esto?

Ryan
fuente
Esta es una coincidencia. ¡Estaba a punto de hacer esta pregunta! Tengo el mismo problema. Factorizando el patrón de IU común en un UserControl, pero queriendo hacer referencia a la IU de contenido por su nombre.
mackenir
3
Este tipo encontró una solución que implicaba deshacerse del archivo XAML de su control personalizado y crear la interfaz de usuario del control personalizado mediante programación. Esta publicación de blog tiene más que decir sobre el tema.
mackenir
2
¿Por qué no usa el método ResourceDictionary? Defina el DataTemplate en él. O use la palabra clave BasedOn para heredar el control. Solo algunos caminos que seguiría antes de hacer la interfaz de usuario de código subyacente en WPF ...
Louis Kottmann

Respuestas:

46

La respuesta es no usar UserControl para hacerlo.

Cree una clase que amplíe 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; }
}

luego usa un estilo para especificar el contenido

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

y finalmente - úsalo

<control:MyFunkyControl Heading="Some heading!">            
    <Label Name="WithAName">Some cool content</Label>
</control:MyFunkyControl>
Sybrand
fuente
2
Encontré que esta es realmente la solución más cómoda, ya que puede desarrollar ControlTemplate en un UserControl ordinario usando el diseñador y transformarlo en un estilo con una plantilla de control asociada.
Oliver Weichhold
5
Hmm, es un poco extraño que funcione para ti, porque intenté aplicar este enfoque y, en mi caso, sigo recibiendo este infame error.
greenoldman
8
@greenoldman @Badiboy Creo que sé por qué no funcionó para ti. probablemente acaba de cambiar un código existente de UserControlheredar ContentControl. Para resolverlo, simplemente agregue una nueva clase ( no XAML con CS). Y luego funcionará (con suerte). si lo desea, he creado una pequeña solución
VS2010
1
Muchos años después y desearía poder votar esto nuevamente :)
Drew Noakes
2
¿Supongo HeadingContainery MyFunkyContainerestán destinados a ser MyFunkyControl?
Martin Schneider
20

Parece que esto no es posible cuando se usa XAML. Los controles personalizados parecen ser exagerados cuando en realidad tengo todos los controles que necesito, pero solo necesito agruparlos con un poco de lógica y permitir contenido con nombre.

La solución en el blog de JD, como sugiere mackenir, parece tener el mejor compromiso. Una forma de extender la solución de JD para permitir que los controles aún se definan en XAML podría ser la siguiente:

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

En mi ejemplo anterior, he creado un control de usuario llamado UserControlDefinedInXAML que se define como cualquier control de usuario normal que usa XAML. En mi UserControlDefinedInXAML tengo un StackPanel llamado aStackPanel dentro del cual quiero que aparezca mi contenido con nombre.

Ryan
fuente
Me he dado cuenta de que tengo problemas de vinculación de datos cuando uso este mecanismo de crianza del contenido. El enlace de datos parece configurado correctamente, pero el llenado inicial de los controles de la fuente de datos no funciona correctamente. Creo que el problema se limita a los controles que no son hijos directos del presentador de contenido.
mackenir
No he tenido la oportunidad de experimentar con esto desde entonces, pero no he tenido ningún problema para enlazar datos con los controles definidos en UserControlDefinedInXAML (del ejemplo anterior) o los controles agregados al ContentPresenter hasta ahora. Sin embargo, he estado vinculando datos solo a través del código (no XAML, no estoy seguro de si eso hace una diferencia).
Ryan
Parece que SI marca la diferencia. Intenté usar XAML para mis enlaces de datos en el caso que describiste y no funciona. Pero si lo configuro en código, ¡funciona!
Ryan
3

Otra alternativa que he usado es simplemente establecer la Namepropiedad en elLoaded evento.

En mi caso, tenía un control bastante complejo que no quería crear en el código subyacente, y buscaba un control opcional con un nombre específico para cierto comportamiento, y como noté que podía establecer el nombre en un DataTemplatePensé que también podría hacerlo en el Loadedevento.

private void Button_Loaded(object sender, RoutedEventArgs e)
{
    Button b = sender as Button;
    b.Name = "buttonName";
}
Raquel
fuente
2
Si hace esto, los enlaces que usan el nombre no funcionarán ... a menos que establezca los enlaces en el código subyacente.
Torre
3

A veces, es posible que solo necesite hacer referencia al elemento de C #. Dependiendo del caso de uso, puede establecer an en x:Uidlugar de an x:Namey acceder a los elementos llamando a un método de búsqueda de Uid como Obtener objeto por su Uid en WPF .

Dani
fuente
1

Puede utilizar este ayudante para establecer el nombre dentro del control de usuario:

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

y su ventana o código de control subyacente establece que controla por propiedad:

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

    }

    public Button BtnOK { get; set; }
}

tu ventana 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 obtiene su nombre de control y su nombre de clase para establecer Control en Propiedad.

Ali Yousefi
fuente
0

Elegí crear una propiedad adicional para cada elemento que necesito obtener:

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

Esto me permite acceder a los elementos secundarios en XAML:

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

Código detrás:

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

La solución real sería que Microsoft solucionara este problema, así como todos los demás con árboles visuales rotos, etc. Hipotéticamente hablando.

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

Otra solución alternativa: haga referencia al elemento como RelativeSource .

voccoeisuoi
fuente
0

Tuve el mismo problema al usar un TabControl al colocar un montón de controles con nombre en.

Mi solución fue usar una plantilla de control que contiene todos mis controles para que se muestren en una página de pestañas. Dentro de la plantilla, puede usar la propiedad Name y también enlazar datos a propiedades del control nombrado desde otros controles al menos dentro de la misma plantilla.

Como Contenido del Control TabItem, use un Control simple y configure ControlTemplate en consecuencia:

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

Para acceder a los controles nombrados dentro de la plantilla desde el código detrás, necesitaría usar el árbol visual.

David Wellna
fuente