So erstellen Sie ein WPF UserControl mit NAMED-Inhalten

100

Ich habe eine Reihe von Steuerelementen mit angehängten Befehlen und Logik, die ständig auf die gleiche Weise wiederverwendet werden. Ich habe beschlossen, ein Benutzersteuerelement zu erstellen, das alle gängigen Steuerelemente und die Logik enthält.

Ich benötige jedoch auch das Steuerelement, um benennbare Inhalte enthalten zu können. Ich habe folgendes versucht:

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

Es scheint jedoch, dass Inhalte, die sich im Benutzersteuerelement befinden, nicht benannt werden können. Zum Beispiel, wenn ich das Steuerelement folgendermaßen verwende:

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

Ich erhalte folgende Fehlermeldung:

Der Name-Attributwert 'buttonName' kann für das Element 'Button' nicht festgelegt werden. 'Button' befindet sich im Bereich des Elements 'UserControl1', für das bereits ein Name registriert war, als er in einem anderen Bereich definiert wurde.

Wenn ich den ButtonName entferne, wird er kompiliert, ich muss jedoch in der Lage sein, den Inhalt zu benennen. Wie kann ich das erreichen?

Ryan
quelle
Das ist ein Zufall. Ich wollte gerade diese Frage stellen! Ich habe das gleiche Problem. Ausklammern des allgemeinen UI-Musters in ein UserControl, aber Verweisen auf die Inhalts-UI mit Namen.
Mackenir
3
Dieser Typ hat eine Lösung gefunden, bei der die XAML-Datei seines benutzerdefinierten Steuerelements entfernt und die Benutzeroberfläche des benutzerdefinierten Steuerelements programmgesteuert erstellt wurde. Dieser Blog-Beitrag hat mehr zu diesem Thema zu sagen.
Mackenir
2
Warum verwenden Sie nicht das ResourceDictionary? Definieren Sie das DataTemplate darin. Oder verwenden Sie das Schlüsselwort BasedOn, um das Steuerelement zu erben. Nur ein paar Pfade, denen ich folgen würde, bevor ich Code-Behind-UI in WPF mache ...
Louis Kottmann

Antworten:

46

Die Antwort ist, kein UserControl zu verwenden, um dies zu tun.

Erstellen Sie eine Klasse, die ContentControl erweitert

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

Verwenden Sie dann einen Stil, um den Inhalt anzugeben

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

und schließlich - benutze es

<control:MyFunkyControl Heading="Some heading!">            
    <Label Name="WithAName">Some cool content</Label>
</control:MyFunkyControl>
Sybrand
quelle
2
Ich fand dies tatsächlich die bequemste Lösung, da Sie die ControlTemplate in einem normalen UserControl mit dem Designer ausarbeiten und in einen Stil mit zugehöriger Control-Vorlage umwandeln können.
Oliver Weichhold
5
Hmm, es ist ein bisschen seltsam, dass es für Sie funktioniert, weil ich versucht habe, diesen Ansatz anzuwenden, und in meinem Fall bekomme ich immer noch diesen berüchtigten Fehler.
Greenoldman
8
@greenoldman @Badiboy Ich glaube ich weiß warum es bei dir nicht funktioniert hat. Sie haben wahrscheinlich gerade einen vorhandenen Code von UserControlin geerbt geändert ContentControl. Fügen Sie zum Lösen einfach eine neue Klasse hinzu ( nicht XAML mit CS). Und dann wird es (hoffentlich) funktionieren. Wenn Sie möchten
itsho
1
Viele Jahre später und ich wünschte, ich könnte dies wieder verbessern :)
Drew Noakes
2
Ich denke HeadingContainerund MyFunkyContainersoll sein MyFunkyControl?!
Martin Schneider
20

Es scheint, dass dies nicht möglich ist, wenn XAML verwendet wird. Benutzerdefinierte Steuerelemente scheinen ein Overkill zu sein, wenn ich tatsächlich alle Steuerelemente habe, die ich benötige, sie aber nur mit ein wenig Logik gruppieren und benannte Inhalte zulassen muss.

Die Lösung auf JDs Blog, wie Mackenir vorschlägt, scheint den besten Kompromiss zu haben. Eine Möglichkeit, die Lösung von JD zu erweitern, damit Steuerelemente weiterhin in XAML definiert werden können, könnte folgende sein:

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

In meinem obigen Beispiel habe ich ein Benutzersteuerelement mit dem Namen UserControlDefinedInXAML erstellt, das wie alle normalen Benutzersteuerelemente mit XAML definiert ist. In meiner UserControlDefinedInXAML habe ich ein StackPanel namens aStackPanel, in dem mein benannter Inhalt angezeigt werden soll.

Ryan
quelle
Ich habe festgestellt, dass bei der Verwendung dieses Mechanismus zur erneuten Elternschaft des Inhalts Probleme mit der Datenbindung auftreten. Die Datenbindung scheint korrekt eingerichtet zu sein, aber das anfängliche Auffüllen der Steuerelemente aus der Datenquelle funktioniert nicht ordnungsgemäß. Ich denke, das Problem beschränkt sich auf Steuerelemente, die nicht das direkte Kind des Inhaltspräsentators sind.
Mackenir
Ich hatte seitdem keine Gelegenheit, damit zu experimentieren, aber ich hatte keine Probleme mit der Datenbindung zu den in der UserControlDefinedInXAML definierten Steuerelementen (aus dem obigen Beispiel) oder den Steuerelementen, die dem ContentPresenter bisher hinzugefügt wurden. Ich habe die Datenbindung jedoch nur über Code durchgeführt (nicht über XAML - ich bin mir nicht sicher, ob dies einen Unterschied macht).
Ryan
Es scheint, dass es einen Unterschied macht. Ich habe gerade versucht, XAML für meine Datenbindungen in dem von Ihnen beschriebenen Fall zu verwenden, und es funktioniert nicht. Aber wenn ich es in Code setze, funktioniert es!
Ryan
3

Eine andere Alternative, die ich verwendet habe, besteht darin, die NameEigenschaft im LoadedEreignis festzulegen.

In meinem Fall hatte ich ein ziemlich komplexes Steuerelement, das ich nicht im CodeBehind erstellen wollte, und es suchte nach einem optionalen Steuerelement mit einem bestimmten Namen für ein bestimmtes Verhalten, und da ich bemerkte, dass ich den Namen in a setzen konnte DataTemplateIch dachte, ich könnte es auch in diesem LoadedFall tun .

private void Button_Loaded(object sender, RoutedEventArgs e)
{
    Button b = sender as Button;
    b.Name = "buttonName";
}
Rachel
quelle
2
Wenn Sie dies tun, funktionieren Bindungen mit dem Namen nicht ... es sei denn, Sie legen die Bindungen im Code dahinter fest.
Turm
3

Manchmal müssen Sie möglicherweise nur auf das Element aus C # verweisen. Abhängig vom Anwendungsfall können Sie dann ein x:Uidanstelle von x:Nameund festlegen und auf die Elemente zugreifen, indem Sie eine Uid-Finder-Methode wie Get object by its Uid in WPF aufrufen .

Dani
quelle
1

Sie können diesen Helfer für den Set-Namen innerhalb des Benutzersteuerelements verwenden:

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

und Ihr Fenster oder Steuercode dahinter legen Sie die Kontrolle über die Eigenschaft fest:

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

    }

    public Button BtnOK { get; set; }
}

Ihr Fenster 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 ruft Ihren Steuerelementnamen und Ihren Klassennamen ab, um Control auf Property zu setzen.

Ali Yousefi
quelle
0

Ich habe beschlossen, für jedes Element, das ich erhalten möchte, eine zusätzliche Eigenschaft zu erstellen:

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

Dadurch kann ich auf die untergeordneten Elemente in XAML zugreifen:

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

Code dahinter:

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

Die wirkliche Lösung wäre, dass Microsoft dieses Problem sowie alle anderen mit gebrochenen visuellen Bäumen usw. behebt. Hypothetisch gesehen.

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

Noch eine Problemumgehung: Verweisen Sie auf das Element als RelativeSource .

voccoeisuoi
quelle
0

Ich hatte das gleiche Problem mit einem TabControl, als ich eine Reihe benannter Steuerelemente in platzierte.

Meine Problemumgehung bestand darin, eine Steuerelementvorlage zu verwenden, die alle meine Steuerelemente enthält, die auf einer Registerkarte angezeigt werden sollen. Innerhalb der Vorlage können Sie die Name-Eigenschaft verwenden und Daten aus anderen Steuerelementen an Eigenschaften des benannten Steuerelements binden, zumindest innerhalb derselben Vorlage.

Verwenden Sie als Inhalt des TabItem-Steuerelements ein einfaches Steuerelement und legen Sie die ControlTemplate entsprechend fest:

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

Wenn Sie über den Code dahinter auf die benannten Steuerelemente in der Vorlage zugreifen, müssen Sie den visuellen Baum verwenden.

David Wellna
quelle