1

У меня имеется много TextBox полей, к каждому из которых имеется свой TextBlock с названием поля. Из-за того, что XAML разрастается до невероятных размеров, а все что указывается в TextBlock - это поле Text (остальное забил общим стилем). Назрел вопрос: как бы сделать элемент управления состоящий из TextBox и TextBlock с форматированием, имеющий в себе все свойства TextBox + поле Text для TextBlock. Пробовал создать его через UserControl, но в таком случае нужно прописывать очень много. Поэтому хотелось бы как-то унаследовать от TextBox. Если кто знает как - пожалуйста кодом. Спасибо.

  • нужно прописывать очень много - чего это например? Что TextBox, что TextBlock - важное там только одно - Text свойство, которое вам достаточно пробросить через свойство зависимости. имеется много TextBox полей - а вот это означает лишь одно - вы делаете странное, ибо если много, значит это коллекция, которая имеет все нужные данные и которая привязана к одному ItemsControl с нужным видом. Если вы коллекцию вручную расписываете в XAML, то чтож, вы ССЗБ (гуглите). – EvgeniyZ Feb 01 '24 at 13:15
  • Ага, понял. У меня просто не так много опыта. Я просто не допер до такой вещи, как обращаться к элементам пользовательского элемента по имени. Так что просто создал два свойства для текстов и задал имена. А на счет кастомного ItemsControl - тут надо будет разобраться. – Дмитрий Feb 01 '24 at 13:51
  • Я вам и слова не говорил про обращение по имени... Это самое ужасное, что можно только сделать в WPF) Привязки - вот ваш главный инструмент, пользуйтесь только им. Если видите в коде textBox.Text = ... или аналог - значит вы делаете что-то не так. – EvgeniyZ Feb 01 '24 at 14:09
  • Нет я имею ввиду, что в XAML я пишу что-то вроде: <local:TestElement x:Name="TestElement" Text="Text" Header="Header" /> А в коде если надо обратиться к тому же TextBox (что-то изменить) через TestElement.TBox.Height = 22; (где TBox собственно имя в TextBox в пользовательском элементе) – Дмитрий Feb 01 '24 at 14:29
  • Точней там будет Text="{Binding Text}" Header="{Binding Header}" – Дмитрий Feb 01 '24 at 14:37
  • А в коде если надо обратиться к тому же TextBox (что-то изменить) через TestElement.TBox.Height = 22;, а потом перечитываем мой комментарий выше Если видите в коде textBox.Text = ... или аналог - значит вы делаете что-то не так.... У вас вообще не должно быть x:Name, вот вообще, от слова совсем. – EvgeniyZ Feb 01 '24 at 14:47

1 Answers1

1

Пример

Предположим, у нас есть такой вид

UI

Написан он следующим образом

XAML

<StackPanel>
    <StackPanel Margin="5">
        <TextBlock x:Name="name1" FontWeight="Medium" />
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Лет: " />
            <TextBox x:Name="age1" />
        </StackPanel>
    </StackPanel>
&lt;StackPanel Margin=&quot;5&quot;&gt;
    &lt;TextBlock x:Name=&quot;name2&quot; FontWeight=&quot;Medium&quot; /&gt;
    &lt;StackPanel Orientation=&quot;Horizontal&quot;&gt;
        &lt;TextBlock Text=&quot;Лет: &quot; /&gt;
        &lt;TextBox x:Name=&quot;age2&quot; /&gt;
    &lt;/StackPanel&gt;
&lt;/StackPanel&gt;

&lt;StackPanel Margin=&quot;5&quot;&gt;
    &lt;TextBlock x:Name=&quot;name3&quot; FontWeight=&quot;Medium&quot; /&gt;
    &lt;StackPanel Orientation=&quot;Horizontal&quot;&gt;
        &lt;TextBlock Text=&quot;Лет: &quot; /&gt;
        &lt;TextBox x:Name=&quot;age3&quot; /&gt;
    &lt;/StackPanel&gt;
&lt;/StackPanel&gt;

&lt;StackPanel Margin=&quot;5&quot;&gt;
    &lt;TextBlock x:Name=&quot;name4&quot; FontWeight=&quot;Medium&quot; /&gt;
    &lt;StackPanel Orientation=&quot;Horizontal&quot;&gt;
        &lt;TextBlock Text=&quot;Лет: &quot; /&gt;
        &lt;TextBox x:Name=&quot;age4&quot; /&gt;
    &lt;/StackPanel&gt;
&lt;/StackPanel&gt;

</StackPanel>

C#

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    name1.Text = &quot;Вася&quot;;
    age1.Text = &quot;13&quot;;

    name2.Text = &quot;Петя&quot;;
    age2.Text = &quot;18&quot;;

    name3.Text = &quot;Маша&quot;;
    age3.Text = &quot;7&quot;;

    name4.Text = &quot;Оля&quot;;
    age4.Text = &quot;24&quot;;
}

}

Многие новички именно так и пишут свой проект, не задумываясь о правильности того, что они делают. Ну а делают они все в корне неверно, ибо самое ключевое в WPF проекте, это XAML и Binding (привязки), если вы ими пренебрегаете, вы очень многого лишаетесь, как в плане удобства, так и в плане производительности.

Как поступить

Первым делом давайте взглянем на вид и подумаем, а не коллекция случаем это? Ведь если что-то имеет повторяющиеся "паттерны", то значит это можно описать одним классом и создать его множество раз, поместив в коллекцию, верно? Сделаем это, получив уже такой C# код:

public record User(string Name, int Age);

public partial class MainWindow : Window { public List<User> Users { get; set; }

public MainWindow()
{
    InitializeComponent();

    Users = [new(&quot;Вася&quot;, 13), new(&quot;Петя&quot;, 18), new(&quot;Маша&quot;, 7), new(&quot;Оля&quot;, 24)];
}

}

Смотрите какой простой и логичный код, где есть конкретный класс User, имеющее в себе только те данные, которые относятся к конкретному человеку. А также есть простая коллекция Users, которая хранит в себе всех этих людей.

Имея коллекцию, давайте теперь адаптируем под нее XAML. За вывод коллекции отвечает ListBox (если нужно выделение) и ItemsControl (если выделение нам не нужно), также есть всякие DataGrid и пр., которые имеют разный вид отображения коллекции. В данном случае нам нужно просто продублировать определенный вид столько раз, сколько объектов в коллекции, а значит подойдет простой ItemsControl. XAML превратится тогда в такое:

<ItemsControl ItemsSource="{Binding Users}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Margin="5">
                <TextBlock FontWeight="Medium" Text="{Binding Name}" />
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="Лет: " />
                    <TextBox Text="{Binding Age}" />
                </StackPanel>
            </StackPanel>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Заметьте, все дубликаты ушли, я только раз написал нужный мне вид и привязал его к коллекции, ничего более.

Чтобы это все заработало, нам надо указать DataContext окну, это некий источник данных, в котором будут искаться все свойства для привязки. В коде выше, коллекцию я создал прям в классе окна, это плохо, лучше вынести в отдельный класс, но для примера сойдет, ну а раз данные у нас в окне, то и указываем окну в конструкторе DataContext = this;.

Как все сделали, запускаем проект и видим все тот-же вид, который и был, с тем-же набором данных, но уже написанный правильно, с использованием привязок. Обратите внимание, в XAML у меня нет ни одного x:Name.

UserControl или как сделать TextBox с наименованием

Поняв теперь как избавиться от дубликатов в коде, можно "сгруппировать" вид нашего "юзера" в один конкретный UserControl, делается это очень просто:

  1. Кликаем ПКМ на название проекта и выбираем "Добавить" - "Пользовательский элемент управления.

  2. Задаем ему нужное имя, в моем случае это будет просто UserView.

  3. Открываем .xaml.cs файл и пишем там после конструктора propdp и жмем TAB, вам студия вставит заготовку для свойства зависимости.

  4. Указываем этому свойству тип, имя, название UserControl, значение по умолчанию.

  5. В моем примере, я сделаю 2 свойство, заголовок и значение, получится следующее

    public string Title
    {
        get => (string)GetValue(TitleProperty);
        set => SetValue(TitleProperty, value);
    }
    

    public object Value { get => GetValue(ValueProperty); set => SetValue(ValueProperty, value); }

    public static readonly DependencyProperty TitleProperty = DependencyProperty.Register("Title", typeof(string), typeof(UserView), new PropertyMetadata(default));

    public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(UserView), new PropertyMetadata(default));

  6. Открываем теперь .xaml файл и переносим туда вид.

  7. В UserControl так просто не привязать данные, вот тут имя и может пригодиться. Задаем самому UserControl значение x:Name, а привязки меняем на {Binding Title, ElementName=uc} (uc - имя, заданное в x:Name).

  8. Все, с контролом закончили. Возвращаемся в основной XAML и от куда забирали вид, меняем на <local:UserView Title="{Binding Name}" Value="{Binding Age}"/>? запускаем и радуемся результатом.

В итоге, весь наш изначальный XAML, с кучей дубликатов и тесной связью с C# кодом, превратился в простой и лаконичный:

<ItemsControl ItemsSource="{Binding Users}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <local:UserView Title="{Binding Name}" Value="{Binding Age}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Вот примерно такое у вас и должно быть.

EvgeniyZ
  • 15,694
  • Чтобы подсветить xaml, надо <!-- language: lang-xml --> через пустую строку перед кодом добавить. – aepot Feb 01 '24 at 22:51
  • Спасибо за ответ, с этим разобрался. Появился еще вопрос: можно ли пойти еще дальше и сделать универсальный элемент для нескольких? Что я имею ввиду: тут у нас TextBox с названием, но у меня так же имеются и другие элементы (ComboBox, DatePicker...). Собственно можно сделать под каждый свой элемент (что в принципе правильно), но все таки интересно, как это можно реализовать в одном универсальном – Дмитрий Feb 02 '24 at 12:46
  • @Дмитрий Это совершенно не верный подход к проектированию. Вот скажем, DataPicker, это что? Наверно это класс, который содержит в себе DateTime свойство. А ComboBox? Наверно это коллекция неких объектов, да еще и идентификатор того, что отображать, а также свойство "выделен сейчас". То есть, все это, совершенно разные объекты, с совершенно разной логикой, пихать все в одно место, это нарушение всех возможных правил программирования. Поэтому, это должно быть отдельными классами, а в XAML должно быть задано соответствие через DataTemplate. – EvgeniyZ Feb 02 '24 at 13:13
  • Понял, спасибо большое – Дмитрий Feb 02 '24 at 13:17