У меня имеется много TextBox полей, к каждому из которых имеется свой TextBlock с названием поля. Из-за того, что XAML разрастается до невероятных размеров, а все что указывается в TextBlock - это поле Text (остальное забил общим стилем). Назрел вопрос: как бы сделать элемент управления состоящий из TextBox и TextBlock с форматированием, имеющий в себе все свойства TextBox + поле Text для TextBlock. Пробовал создать его через UserControl, но в таком случае нужно прописывать очень много. Поэтому хотелось бы как-то унаследовать от TextBox. Если кто знает как - пожалуйста кодом. Спасибо.
1 Answers
Пример
Предположим, у нас есть такой вид
Написан он следующим образом
XAML
<StackPanel>
<StackPanel Margin="5">
<TextBlock x:Name="name1" FontWeight="Medium" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="Лет: " />
<TextBox x:Name="age1" />
</StackPanel>
</StackPanel>
<StackPanel Margin="5">
<TextBlock x:Name="name2" FontWeight="Medium" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="Лет: " />
<TextBox x:Name="age2" />
</StackPanel>
</StackPanel>
<StackPanel Margin="5">
<TextBlock x:Name="name3" FontWeight="Medium" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="Лет: " />
<TextBox x:Name="age3" />
</StackPanel>
</StackPanel>
<StackPanel Margin="5">
<TextBlock x:Name="name4" FontWeight="Medium" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="Лет: " />
<TextBox x:Name="age4" />
</StackPanel>
</StackPanel>
</StackPanel>
C#
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
name1.Text = "Вася";
age1.Text = "13";
name2.Text = "Петя";
age2.Text = "18";
name3.Text = "Маша";
age3.Text = "7";
name4.Text = "Оля";
age4.Text = "24";
}
}
Многие новички именно так и пишут свой проект, не задумываясь о правильности того, что они делают. Ну а делают они все в корне неверно, ибо самое ключевое в 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("Вася", 13), new("Петя", 18), new("Маша", 7), new("Оля", 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, делается это очень просто:
Кликаем ПКМ на название проекта и выбираем "Добавить" - "Пользовательский элемент управления.
Задаем ему нужное имя, в моем случае это будет просто
UserView.Открываем
.xaml.csфайл и пишем там после конструктораpropdpи жмем TAB, вам студия вставит заготовку для свойства зависимости.Указываем этому свойству тип, имя, название
UserControl, значение по умолчанию.В моем примере, я сделаю 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));
Открываем теперь
.xamlфайл и переносим туда вид.В
UserControlтак просто не привязать данные, вот тут имя и может пригодиться. Задаем самомуUserControlзначениеx:Name, а привязки меняем на{Binding Title, ElementName=uc}(uc- имя, заданное вx:Name).Все, с контролом закончили. Возвращаемся в основной 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>
Вот примерно такое у вас и должно быть.
- 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 -

нужно прописывать очень много- чего это например? ЧтоTextBox, чтоTextBlock- важное там только одно -Textсвойство, которое вам достаточно пробросить через свойство зависимости.имеется много TextBox полей- а вот это означает лишь одно - вы делаете странное, ибо если много, значит это коллекция, которая имеет все нужные данные и которая привязана к одномуItemsControlс нужным видом. Если вы коллекцию вручную расписываете в XAML, то чтож, вы ССЗБ (гуглите). – EvgeniyZ Feb 01 '24 at 13:15textBox.Text = ...или аналог - значит вы делаете что-то не так. – EvgeniyZ Feb 01 '24 at 14:09<local:TestElement x:Name="TestElement" Text="Text" Header="Header" />А в коде если надо обратиться к тому же TextBox (что-то изменить) черезTestElement.TBox.Height = 22;(где TBox собственно имя в TextBox в пользовательском элементе) – Дмитрий Feb 01 '24 at 14:29Text="{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