Jak utworzyć kontrolkę użytkownika WPF z zawartością nazwaną

100

Mam zestaw kontrolek z dołączonymi poleceniami i logiką, które są stale ponownie używane w ten sam sposób. Zdecydowałem się stworzyć kontrolkę użytkownika, która zawiera wszystkie wspólne kontrolki i logikę.

Jednak potrzebuję również kontrolki, aby móc przechowywać zawartość, którą można nazwać. Wypróbowałem następujące:

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

Jednak wydaje się, że nie można nazwać żadnej zawartości umieszczonej w kontrolce użytkownika. Na przykład, jeśli używam kontrolki w następujący sposób:

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

Otrzymuję następujący błąd:

Nie można ustawić wartości atrybutu nazwy „buttonName” w elemencie „Button”. „Przycisk” znajduje się w zakresie elementu „UserControl1”, który miał już zarejestrowaną nazwę, gdy została zdefiniowana w innym zakresie.

Jeśli usunę buttonName, zostanie on skompilowany, ale muszę mieć możliwość nazwania zawartości. Jak mogę to osiągnąć?

Ryan
źródło
To zbieg okoliczności. Właśnie miałem zadać to pytanie! Mam ten sam problem. Uwzględniając wspólny wzorzec interfejsu użytkownika w UserControl, ale chcąc odwoływać się do interfejsu użytkownika zawartości według nazwy.
mackenir
3
Ten facet znalazł rozwiązanie polegające na pozbyciu się pliku XAML kontrolki niestandardowej i programistycznym utworzeniu interfejsu użytkownika kontrolki niestandardowej. Ten post na blogu ma więcej do powiedzenia na ten temat.
mackenir
2
Dlaczego nie używasz sposobu ResourceDictionary? Zdefiniuj w nim DataTemplate. Lub użyj słowa kluczowego BasedOn, aby odziedziczyć formant. Tylko kilka ścieżek, którymi podążałem przed zrobieniem interfejsu użytkownika związanego z kodem w WPF ...
Louis Kottmann

Odpowiedzi:

46

Odpowiedź brzmi: nie używaj do tego UserControl.

Utwórz klasę, która rozszerza 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; }
}

następnie użyj stylu, aby określić zawartość

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

i wreszcie - użyj go

<control:MyFunkyControl Heading="Some heading!">            
    <Label Name="WithAName">Some cool content</Label>
</control:MyFunkyControl>
Sybrand
źródło
2
Okazało się, że jest to najbardziej wygodne rozwiązanie, ponieważ można rozwinąć ControlTemplate w zwykłym UserControl przy użyciu projektanta i przekształcić go w styl z powiązanym szablonem sterowania.
Oliver Weichhold,
5
Hmm, to trochę dziwne, że działa u Ciebie, ponieważ próbowałem zastosować takie podejście iw moim przypadku nadal otrzymuję ten niesławny błąd.
greenoldman
8
@greenoldman @Badiboy Myślę, że wiem, dlaczego to nie zadziałało. prawdopodobnie właśnie zmieniłeś istniejący kod z UserControlna dziedziczenie ContentControl. Aby rozwiązać, po prostu dodaj nową klasę ( nie XAML z CS). I wtedy (miejmy nadzieję) zadziała. jeśli chcesz, stworzyłem małe rozwiązanie VS2010
itsho
1
Wiele lat później i chciałbym móc ponownie zagłosować :)
Drew Noakes
2
Chyba HeadingContaineri MyFunkyContainertak powinno być MyFunkyControl?!
Martin Schneider
20

Wydaje się, że nie jest to możliwe, gdy używany jest język XAML. Niestandardowe kontrolki wydają się przesadą, kiedy faktycznie mam wszystkie potrzebne kontrolki, ale wystarczy zgrupować je razem z odrobiną logiki i zezwolić na nazwane treści.

Rozwiązanie na blogu JD, jak sugeruje mackenir, wydaje się mieć najlepszy kompromis. Sposób na rozszerzenie rozwiązania JD, aby umożliwić dalsze definiowanie formantów w języku XAML, może wyglądać następująco:

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

W powyższym przykładzie utworzyłem kontrolkę użytkownika o nazwie UserControlDefinedInXAML, która jest zdefiniowana jak każda normalna kontrolka użytkownika przy użyciu XAML. W moim UserControlDefinedInXAML mam StackPanel o nazwie aStackPanel, w którym chcę, aby pojawiła się moja nazwana zawartość.

Ryan
źródło
Zauważyłem, że podczas korzystania z tego mechanizmu ponownego rodzicielstwa zawartości pojawiają się problemy z wiązaniem danych. Wydaje się, że powiązanie danych jest poprawnie skonfigurowane, ale początkowe wypełnianie formantów ze źródła danych nie działa poprawnie. Myślę, że problem ogranicza się do elementów sterujących, które nie są bezpośrednim dzieckiem prezentera treści.
mackenir
Od tego czasu nie miałem okazji eksperymentować z tym, ale nie miałem żadnych problemów z wiązaniem danych ani z kontrolkami zdefiniowanymi w UserControlDefinedInXAML (z powyższego przykładu), ani z kontrolkami dodanymi do tej pory do ContentPresenter. Jednak wiązałem dane tylko za pomocą kodu (nie XAML - nie jestem pewien, czy to robi różnicę).
Ryan
Wygląda na to, że to robi różnicę. Właśnie próbowałem użyć XAML dla moich powiązań danych w przypadku, który opisałeś i nie działa. Ale jeśli ustawię to w kodzie, zadziała!
Ryan
3

Inną alternatywą, której użyłem, jest po prostu ustawienie Namewłaściwości w Loadedzdarzeniu.

W moim przypadku miałem dość złożoną kontrolkę, której nie chciałem tworzyć w kodzie, i szukała ona opcjonalnej kontrolki o określonej nazwie dla określonego zachowania, a ponieważ zauważyłem, że mogę ustawić nazwę w DataTemplatePomyślałem, że mogę to zrobić również w Loadedprzypadku.

private void Button_Loaded(object sender, RoutedEventArgs e)
{
    Button b = sender as Button;
    b.Name = "buttonName";
}
Rachel
źródło
2
Jeśli to zrobisz, powiązania używające tej nazwy nie będą działać ... chyba że ustawisz powiązania w kodzie za.
Wieża
3

Czasami wystarczy odwołać się do elementu z C #. W zależności od przypadku użycia można następnie ustawić x:Uidzamiast an x:Namei uzyskać dostęp do elementów, wywołując metodę Uid Finder, taką jak Get object według jego Uid w WPF .

Dani
źródło
1

Możesz użyć tego pomocnika do ustawienia nazwy w kontrolce użytkownika:

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

a Twój Window lub Control Code Behind ustawia kontrolę według właściwości:

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

    }

    public Button BtnOK { get; set; }
}

twoje okno 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 uzyskuje nazwę kontrolki i nazwę klasy, aby ustawić kontrolę na Właściwość.

Ali Yousefi
źródło
0

Zdecydowałem się utworzyć dodatkową właściwość dla każdego elementu, który potrzebuję:

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

Umożliwia mi to dostęp do elementów podrzędnych w XAML:

<TextBlock Text="{Binding First.SelectedItem, ElementName=Taxcode}"/>
aliceraunsbaek
źródło
0
<Popup>
    <TextBox Loaded="BlahTextBox_Loaded" />
</Popup>

Kod za:

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

Prawdziwym rozwiązaniem byłoby naprawienie tego problemu przez Microsoft, a także wszystkich innych z uszkodzonymi drzewami wizualnymi itp. Hipotetycznie.

15ee8f99-57ff-4f92-890c-b56153
źródło
0

Jeszcze jedno obejście: odwołaj się do elementu jako RelativeSource .

voccoeisuoi
źródło
0

Miałem ten sam problem, używając TabControl podczas umieszczania zestawu nazwanych kontrolek w.

Moje obejście polegało na użyciu szablonu kontrolnego, który zawiera wszystkie moje kontrolki, które mają być wyświetlane na stronie karty. Wewnątrz szablonu można użyć właściwości Name, a także powiązać dane z właściwościami nazwanej kontrolki z innych kontrolek przynajmniej w tym samym szablonie.

Jako zawartość kontrolki TabItem użyj prostej kontrolki i odpowiednio ustaw ControlTemplate:

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

Uzyskanie dostępu do nazwanej kontrolki w szablonie z kodu znajdującego się za Tobą wymagałoby użycia drzewa wizualnego.

David Wellna
źródło