Come creare un UserControl WPF con contenuto NAMED


100

Ho una serie di controlli con comandi e logica allegati che vengono costantemente riutilizzati allo stesso modo. Ho deciso di creare un controllo utente che contenga tutti i controlli e la logica comuni.

Tuttavia, ho anche bisogno del controllo per poter contenere contenuti che possono essere nominati. Ho provato quanto segue:

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

Tuttavia sembra che non sia possibile nominare alcun contenuto inserito all'interno del controllo utente. Ad esempio se utilizzo il controllo nel modo seguente:

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

Ricevo il seguente errore:

Impossibile impostare il valore dell'attributo Name "buttonName" sull'elemento "Button". "Button" rientra nell'ambito dell'elemento "UserControl1", che aveva già un nome registrato quando è stato definito in un altro ambito.

Se rimuovo buttonName, viene compilato, tuttavia devo essere in grado di assegnare un nome al contenuto. Come posso raggiungere questo obiettivo?


Questa è una coincidenza. Stavo per fare questa domanda! Ho lo stesso problema. Factoring out pattern UI comune in un UserControl, ma si desidera fare riferimento all'interfaccia utente del contenuto per nome.
mackenir

3
Questo ragazzo ha trovato una soluzione che implicava l'eliminazione del file XAML del controllo personalizzato e la creazione dell'interfaccia utente del controllo personalizzato a livello di codice. Questo post sul blog ha altro da dire sull'argomento.
mackenir

2
Perché non usi il modo ResourceDictionary? Definisci il DataTemplate in esso. Oppure usa la parola chiave BasedOn per ereditare il controllo. Solo alcuni percorsi che avrei seguito prima di eseguire l'interfaccia utente code-behind in WPF ...
Louis Kottmann

Risposte:


46

La risposta è non utilizzare un UserControl per farlo.

Crea una classe che 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; }
}

quindi utilizzare uno stile per specificare i contenuti

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

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

2
Ho trovato che questa è effettivamente la soluzione più comoda poiché puoi arricchire il ControlTemplate in un normale UserControl usando il designer e questo lo trasforma in uno stile con il modello di controllo associato.
Oliver Weichhold

5
Hmm, è un po 'strano che funzioni per te, perché ho provato ad applicare questo approccio e nel mio caso ricevo ancora questo famigerato errore.
greenoldman

8
@greenoldman @Badiboy Penso di sapere perché non ha funzionato per te. probabilmente hai appena cambiato un codice esistente da UserControla inherit ContentControl. Per risolvere, aggiungi semplicemente una nuova classe ( non XAML con CS). E poi funzionerà (si spera). se vuoi, ho creato una piccola soluzione VS2010
itsho

1
Molti anni dopo e vorrei poter votare di nuovo a favore :)
Drew Noakes il

2
Immagino HeadingContainere MyFunkyContainerdovrebbero essere MyFunkyControl?!
Martin Schneider

20

Sembra che questo non sia possibile quando viene utilizzato XAML. I controlli personalizzati sembrano essere eccessivi quando ho effettivamente tutti i controlli di cui ho bisogno, ma devo solo raggrupparli insieme con un po 'di logica e consentire il contenuto con nome.

La soluzione sul blog di JD, come suggerisce mackenir, sembra avere il miglior compromesso. Un modo per estendere la soluzione di JD per consentire la definizione dei controlli in XAML potrebbe essere il seguente:

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

Nel mio esempio sopra ho creato un controllo utente chiamato UserControlDefinedInXAML che è definito come qualsiasi normale controllo utente utilizzando XAML. Nel mio UserControlDefinedInXAML ho uno StackPanel chiamato aStackPanel all'interno del quale voglio che appaia il mio contenuto denominato.


Ho riscontrato problemi di associazione dei dati quando utilizzo questo meccanismo di riprogettazione del contenuto. Il data-binding sembra impostato correttamente, ma il popolamento iniziale dei controlli dall'origine dati non funziona correttamente. Penso che il problema sia limitato ai controlli che non sono figli diretti del presentatore di contenuti.
mackenir

Non ho avuto la possibilità di sperimentarlo da allora, ma non ho avuto problemi di associazione dei dati ai controlli definiti in UserControlDefinedInXAML (dall'esempio sopra) o ai controlli aggiunti al ContentPresenter finora. Ho eseguito l'associazione dati solo tramite codice (non XAML, non sono sicuro se questo fa la differenza).
Ryan

Sembra che faccia la differenza. Ho appena provato a utilizzare XAML per le mie associazioni di dati nel caso che hai descritto e non funziona. Ma se lo imposto in codice, funziona!
Ryan,

3

Un'altra alternativa che ho usato è semplicemente impostare la Nameproprietà Loadednell'evento.

Nel mio caso, avevo un controllo piuttosto complesso che non volevo creare nel code-behind e cercava un controllo opzionale con un nome specifico per un determinato comportamento, e poiché ho notato che potevo impostare il nome in un DataTemplateHo pensato di poterlo fare anche in questo Loadedcaso.

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

2
Se si esegue questa operazione, le associazioni che utilizzano il nome non funzioneranno ... a meno che non si impostino le associazioni nel codice sottostante.
Torre

3

A volte potrebbe essere necessario fare riferimento all'elemento da C #. A seconda del caso d'uso, puoi quindi impostare un x:Uidinvece di un x:Namee accedere agli elementi chiamando un metodo di ricerca Uid come Get object dal suo Uid in WPF .


1

Puoi utilizzare questo helper per impostare il nome all'interno del controllo utente:

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 la finestra o il codice di controllo dietro impostano il controllo per proprietà:

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

    }

    public Button BtnOK { get; set; }
}

la tua finestra 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 ottiene il nome del controllo e il nome della classe per impostare Control su Property.


0

Ho scelto di creare una proprietà extra per ogni elemento che devo ottenere:

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

Ciò mi consente di accedere agli elementi figlio in XAML:

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

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

Codice dietro:

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

La vera soluzione sarebbe che Microsoft risolvesse questo problema, così come tutti gli altri con alberi visivi rotti, ecc. Ipoteticamente parlando.


0

Ancora un'altra soluzione alternativa: fare riferimento all'elemento come RelativeSource .


0

Ho avuto lo stesso problema usando un TabControl quando ho inserito un gruppo di controlli con nome in.

La mia soluzione è stata quella di utilizzare un modello di controllo che contiene tutti i miei controlli da mostrare in una scheda. All'interno del modello è possibile utilizzare la proprietà Name e anche il data bind alle proprietà del controllo denominato da altri controlli almeno all'interno dello stesso modello.

Come contenuto del controllo TabItem, usa un semplice controllo e imposta il ControlTemplate di conseguenza:

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

Per accedere ai controlli denominati all'interno del modello dal codice sottostante, è necessario utilizzare l'albero visivo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.