Comment créer un UserControl WPF avec du contenu NAMED


100

J'ai un ensemble de contrôles avec des commandes et une logique attachées qui sont constamment réutilisées de la même manière. J'ai décidé de créer un contrôle utilisateur contenant tous les contrôles et la logique communs.

Cependant, j'ai également besoin du contrôle pour pouvoir contenir du contenu pouvant être nommé. J'ai essayé ce qui suit:

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

Cependant, il semble que tout contenu placé dans le contrôle utilisateur ne puisse pas être nommé. Par exemple, si j'utilise le contrôle de la manière suivante:

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

Je reçois l'erreur suivante:

Impossible de définir la valeur d'attribut Name 'buttonName' sur l'élément 'Button'. 'Button' est sous la portée de l'élément 'UserControl1', qui avait déjà un nom enregistré lors de sa définition dans une autre portée.

Si je supprime le buttonName, il se compile, mais je dois pouvoir nommer le contenu. Comment puis-je atteindre cet objectif?


C'est une coïncidence. J'étais sur le point de poser cette question! J'ai le même problème. Factorisation du modèle d'interface utilisateur commun dans un UserControl, mais souhaitant faire référence à l'interface utilisateur de contenu par son nom.
mackenir

3
Ce type a trouvé une solution consistant à se débarrasser du fichier XAML de son contrôle personnalisé et à créer l'interface utilisateur du contrôle personnalisé par programme. Ce billet de blog a plus à dire sur le sujet.
mackenir

2
Pourquoi n'utilisez-vous pas la méthode ResourceDictionary? Définissez-y le DataTemplate. Ou utilisez le mot clé BasedOn pour hériter du contrôle. Juste quelques chemins que je suivrais avant de faire une interface utilisateur code-behind dans WPF ...
Louis Kottmann

Réponses:


46

La réponse est de ne pas utiliser un UserControl pour le faire.

Créer une classe qui étend 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; }
}

puis utilisez un style pour spécifier le contenu

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

et enfin - utilisez-le

<control:MyFunkyControl Heading="Some heading!">            
    <Label Name="WithAName">Some cool content</Label>
</control:MyFunkyControl>

2
J'ai trouvé que c'était en fait la solution la plus confortable car vous pouvez étoffer le ControlTemplate dans un UserControl ordinaire à l'aide du concepteur et le transformer en un style avec un modèle de contrôle associé.
Oliver Weichhold

5
Hmm, c'est un peu étrange que cela fonctionne pour vous, car j'ai essayé d'appliquer cette approche et dans mon cas, j'obtiens toujours cette erreur infâme.
greenoldman

8
@greenoldman @Badiboy Je pense que je sais pourquoi cela n'a pas fonctionné pour vous. vous venez probablement de changer un code existant de UserControlen hériter ContentControl. Pour résoudre, ajoutez simplement une nouvelle classe ( pas XAML avec CS). Et puis cela fonctionnera (espérons-le). si vous le souhaitez, j'ai créé une petite solution
VS2010

1
De nombreuses années plus tard et j'aimerais pouvoir voter à nouveau :)
Drew Noakes

2
Je suppose HeadingContaineret MyFunkyContainersont censés être MyFunkyControl?!
Martin Schneider

20

Il semble que cela ne soit pas possible lorsque XAML est utilisé. Les contrôles personnalisés semblent exagérés lorsque j'ai en fait tous les contrôles dont j'ai besoin, mais que j'ai juste besoin de les regrouper avec un peu de logique et d'autoriser le contenu nommé.

La solution sur le blog de JD comme le suggère mackenir, semble avoir le meilleur compromis. Un moyen d'étendre la solution de JD pour permettre aux contrôles d'être toujours définis en XAML pourrait être le suivant:

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

Dans mon exemple ci-dessus, j'ai créé un contrôle utilisateur appelé UserControlDefinedInXAML qui est défini comme n'importe quel contrôle utilisateur normal utilisant XAML. Dans mon UserControlDefinedInXAML, j'ai un StackPanel appelé aStackPanel dans lequel je veux que mon contenu nommé apparaisse.


J'ai constaté que j'obtenais des problèmes de liaison de données lors de l'utilisation de ce mécanisme de re-parentage du contenu. La liaison de données semble configurée correctement, mais le remplissage initial des contrôles à partir de la source de données ne fonctionne pas correctement. Je pense que le problème est limité aux contrôles qui ne sont pas les enfants directs du présentateur de contenu.
mackenir

Je n'ai pas eu la chance d'expérimenter cela depuis, mais je n'ai eu aucun problème de liaison de données aux contrôles définis dans UserControlDefinedInXAML (de l'exemple ci-dessus) ou aux contrôles ajoutés au ContentPresenter jusqu'à présent. Cependant, j'ai lié des données uniquement via le code (pas XAML - je ne sais pas si cela fait une différence).
Ryan

Il semble que cela fasse une différence. J'ai juste essayé d'utiliser XAML pour mes liaisons de données dans le cas que vous avez décrit et cela ne fonctionne pas. Mais si je le place dans le code, cela fonctionne!
Ryan

3

Une autre alternative que j'ai utilisée consiste simplement à définir la Namepropriété dans l' Loadedévénement.

Dans mon cas, j'avais un contrôle assez complexe que je ne voulais pas créer dans le code-behind, et il cherchait un contrôle optionnel avec un nom spécifique pour un certain comportement, et depuis que j'ai remarqué que je pouvais définir le nom dans un DataTemplateJ'ai pensé que je pourrais le faire aussi dans l' Loadedévénement.

private void Button_Loaded(object sender, RoutedEventArgs e)
{
    Button b = sender as Button;
    b.Name = "buttonName";
}

2
Si vous faites cela, les liaisons utilisant le nom ne fonctionneront pas ... sauf si vous définissez les liaisons dans le code derrière.
Tour du

3

Parfois, vous devrez peut-être simplement référencer l'élément à partir de C #. Selon le cas d'utilisation, vous pouvez ensuite définir un x:Uidau lieu d'un x:Nameet accéder aux éléments en appelant une méthode de recherche Uid comme Get object par son Uid dans WPF .


1

Vous pouvez utiliser cette aide pour définir le nom dans le contrôle utilisateur:

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

et votre fenêtre ou code de contrôle derrière vous définissez le contrôle par propriété:

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

    }

    public Button BtnOK { get; set; }
}

votre fenêtre 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 récupère votre nom de contrôle et votre nom de classe pour définir Control sur Property.


0

J'ai choisi de créer une propriété supplémentaire pour chaque élément dont j'ai besoin:

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

Cela me permet d'accéder aux éléments enfants en XAML:

<TextBlock Text="{Binding First.SelectedItem, ElementName=Taxcode}"/>

0
<Popup>
    <TextBox Loaded="BlahTextBox_Loaded" />
</Popup>

Code derrière:

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

La vraie solution serait que Microsoft corrige ce problème, ainsi que tous les autres avec des arbres visuels cassés, etc. Hypothétiquement parlant.


0

Encore une autre solution de contournement: faites référence à l'élément en tant que RelativeSource .


0

J'ai eu le même problème en utilisant un TabControl en plaçant un tas de contrôles nommés dans.

Ma solution de contournement était d'utiliser un modèle de contrôle qui contient tous mes contrôles à afficher dans une page à onglet. À l'intérieur du modèle, vous pouvez utiliser la propriété Name et également lier des données aux propriétés du contrôle nommé à partir d'autres contrôles au moins à l'intérieur du même modèle.

En tant que contenu du contrôle TabItem, utilisez un contrôle simple et définissez le ControlTemplate en conséquence:

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

Pour accéder à ces contrôles nommés à l'intérieur du modèle à partir du code derrière, vous devez utiliser l'arborescence visuelle.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.