Если кто-то может помочь мне, прежде чем я сойду с ума. У меня есть пользовательский элемент управления, который содержит ListBox Я хотел бы добавить свойство для SelectedItem в UserControl, чтобы родитель мог его получить. Поэтому я использовал DependencyProperty
Пользовательский контроль (VersionList.xaml):
<UserControl
x:Class="PcVueLauncher.Controls.VersionsList"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:PcVueLauncher.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PcVueLauncher.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:Background="white"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<FrameworkElement.Resources>
<ResourceDictionary>
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</ResourceDictionary>
</FrameworkElement.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Padding="10"
Text="Versions" />
<ListBox
Grid.Row="1"
d:ItemsSource="{d:SampleData ItemCount=5}"
ItemsSource="{Binding Versions}"
SelectedItem="{Binding SelectedVersion}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="5,5,10,5"
Text="{Binding VersionName}" />
<Button
Grid.Column="1"
Padding="5"
Command="{Binding RemoveVersionCommand}"
Content="Remove"
Visibility="{Binding CanBeRemoved, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>
ViewModel, связанная с UserControl (VersionListViewModel)
namespace PcVueLauncher.ViewModels.Controls
{
public class VersionsListViewModel : ViewModelBase
{
private List<VersionPcVue> _versions;
public List<VersionPcVue> Versions
{
get
{
return _versions;
}
set
{
_versions = value;
OnPropertyChanged(nameof(Versions));
}
}
private VersionPcVue _selectedVersion;
public VersionPcVue SelectedVersion
{
get
{
return _selectedVersion;
}
set
{
_selectedVersion = value;
OnPropertyChanged(nameof(SelectedVersion));
}
}
public ICommand RemoveVersionCommand { get; }
public VersionsListViewModel()
{
List<VersionPcVue> versionPcVues = new()
{
new VersionPcVue{VersionName="V15"},
new VersionPcVue{VersionName="V12"}
};
Versions = versionPcVues;
}
}
}
Код UserControl позади (VersionList.cs):
public partial class VersionsList : UserControl
{
public VersionsList()
{
InitializeComponent();
}
public VersionPcVue SelectedVersion
{
get { return (VersionPcVue)GetValue(SelectedVersionProperty); }
set { SetValue(SelectedVersionProperty, value); }
}
//Using a DependencyProperty as the backing store for SelectedVersion.This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedVersionProperty =
DependencyProperty.Register("SelectedVersion",
typeof(VersionPcVue),
typeof(VersionsList),
new FrameworkPropertyMetadata(
defaultValue: null,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback: new PropertyChangedCallback(OnSelectionChanged)));
private static void OnSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.CoerceValue(SelectedVersionProperty);
}
}
// Register a dependency property with the specified property name,
// property type, owner type, property metadata, and callbacks.
public static readonly DependencyProperty SelectedVersionProperty = DependencyProperty.Register(
name: "SelectedVersion",
propertyType: typeof(VersionPcVue),
ownerType: typeof(VersionsList),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: null,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback: new PropertyChangedCallback(OnSelectionChanged)
));
private static void OnSelectionChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
depObj.CoerceValue(SelectedVersionProperty);
}
В HomeView, который содержит UserControl, у меня есть это:
<UserControl
x:Class="PcVueLauncher.Views.HomeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Controls="clr-namespace:PcVueLauncher.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PcVueLauncher.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:system="clr-namespace:System;assembly=netstandard"
xmlns:viewmodels="clr-namespace:PcVueLauncher.ViewModels"
d:Background="White"
d:DataContext="{d:DesignInstance Type=viewmodels:HomeViewModel}"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Controls:VersionsList
x:Name="test"
Grid.Column="0"
DataContext="{Binding VersionsListViewModel}"
SelectedVersion="{Binding DataContext.SelectedVersion, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</UserControl>
И в связанной ViewModel (HomeViewModel)
public class HomeViewModel : ViewModelBase
{
private IProjectService _projectService;
private VersionPcVue _selectedVersion;
public VersionPcVue SelectedVersion
{
get
{
return _selectedVersion;
}
set
{
_selectedVersion = value;
OnPropertyChanged(nameof(SelectedVersion));
}
}
private VersionPcVue _test1;
public VersionPcVue Test1
{
get
{
return _test1;
}
set
{
_test1 = value;
OnPropertyChanged(nameof(Test1));
}
}
private string _test;
public string Test
{
get
{
return _test;
}
set
{
_test = value;
OnPropertyChanged(nameof(Test));
}
}
private VersionsListViewModel versionsListViewModel;
public VersionsListViewModel VersionsListViewModel
{
get
{
return versionsListViewModel;
}
set
{
versionsListViewModel = value;
OnPropertyChanged(nameof(VersionsListViewModel));
}
}
public HomeViewModel(IProjectService projectService)
{
_projectService = projectService;
VersionsListViewModel = new();
}
}
Когда я изменяю выбранный элемент из своего пользовательского элемента управления, в HomeViewModel ничего не происходит. Я думал об ошибке привязки, но чтобы попробовать, я изменил это
SelectedVersion="{Binding DataContext.SelectedVersionnnnnnn, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type Grid}}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
И Visual Studio сообщает мне, что SelectedVersionnnnn не существует в HomeViewModel.
Почему я не могу вернуть выбранный элемент обратно в свойство SelectedVersion моей модели HomeViewModel.
Большое спасибо за твою помощь
Пожалуйста, покажите хотя бы DataContext UserControl (чтобы мы могли проверить ваши привязки) и то, как ListBox связан со свойством зависимости.
В VersionList.xaml
:
<ListBox SelectedItem="{Binding SelectedVersion}" ...
Это связывает только ListBox.SelectedItem
с {DataContext}.SelectedVersion
. Затем, когда элемент выбран, свойство зависимости VersionList.SelectedVersion
не обновляется.
Решение 1. По модели представления (без свойства зависимости)
Я думаю, вы перепутали, потому что пытаетесь использовать сложное свойство зависимости. Простой способ — напрямую использовать модель представления без свойства зависимости.
В VersionsList.cs
удалите SelectedVersionProperty
и SelectedVersion
участников.
Держите VersionList.xaml
с :
<UserControl x:Class="PcVueLauncher.Controls.VersionsList" />
...
<ListBox
...
ItemsSource="{Binding Versions}"
SelectedItem="{Binding SelectedVersion}">
...
</UserControl>
Итак, ListBox.SelectedItem
привязан к ListBox.DataContext.SelectedVersion
. Если ListBox.DataContext
равно VersionsListViewModel
, то ListBox.SelectedItem
связано с VersionsListViewModel.SelectedVersion
.
В родительском контроле HomeView
нужно только передать VersionsListViewModel
в VersionList.DataContext
:
<UserControl x:Class="PcVueLauncher.Views.HomeView"
...
<Controls:VersionsList DataContext="{Binding VersionsListViewModel}" />
...
</UserControl>
Итак, HomeView.VersionsList.ListBox.SelectedItem
привязан HomeView.DataContext.VersionsListViewModel.SelectedVersion
. Если HomeView.DataContext
есть HomeViewModel
, то HomeView.VersionsList.ListBox.SelectedItem
связывается HomeViewModel.VersionsListViewModel.SelectedVersion
.
Наконец, вы можете удалить участника HomeViewModel.SelectedVersion
и использовать HomeViewModel.VersionsListViewModel.SelectedVersion
.
Если вы хотите сохранить участника HomeViewModel.SelectedVersion
, вам нужно перенаправить HomeViewModel.SelectedVersion
на HomeViewModel.VersionsListViewModel.SelectedVersion
в HomeViewModel.cs
:
public class HomeViewModel : ViewModelBase
{
private VersionsListViewModel versionsListViewModel;
public VersionsListViewModel VersionsListViewModel
{
get
{
return versionsListViewModel;
}
set
{
if(versionsListViewModel != null)
versionsListViewModel.PropertyChanged -= VersionsListViewModel_PropertyChanged;
versionsListViewModel = value;
if(versionsListViewModel != null)
versionsListViewModel.PropertyChanged += VersionsListViewModel_PropertyChanged;
OnPropertyChanged(nameof(VersionsListViewModel));
}
}
public VersionPcVue SelectedVersion
{
get
{
return versionsListViewModel.SelectedVersion;
}
set
{
versionsListViewModel.SelectedVersion = value;
OnPropertyChanged(nameof(SelectedVersion));
}
}
void VersionsListViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//Propagate the property changed SelectedVersion
if(string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == nameof(VersionsListViewModel.SelectedVersion))
OnPropertyChanged(nameof(SelectedVersion));
}
}
Хитрость заключается в том, что когда HomeViewModel.VersionsListViewModel.SelectedVersion
изменяется, также уведомляйте об изменении HomeViewModel.SelectedVersion
.
Решение 2. По свойству зависимости
Таким образом, вы хотите, чтобы при выборе элемента, который устанавливает выбранный элемент в VersionsList.SelectedVersion
, вам просто нужно привязать ListBox.SelectedItem
к VersionsList.SelectedVersion
.
Сначала добавьте свойство зависимости SelectedVersion
в VersionList.cs
:
public partial class VersionsList : UserControl
{
public VersionsList()
{
InitializeComponent();
}
public VersionPcVue SelectedVersion
{
get { return (VersionPcVue)GetValue(SelectedVersionProperty); }
set { SetValue(SelectedVersionProperty, value); }
}
public static readonly DependencyProperty SelectedVersionProperty =
DependencyProperty.Register(
"SelectedVersion",
typeof(VersionPcVue),
typeof(VersionsList),
new FrameworkPropertyMetadata(
defaultValue: null,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure
)
);
public List<VersionPcVue> Versions
{
get { return (List<VersionPcVue>)GetValue(VersionsProperty); }
set { SetValue(VersionsProperty, value); }
}
public static readonly DependencyProperty VersionsProperty =
DependencyProperty.Register(
"Versions",
typeof(List<VersionPcVue>),
typeof(VersionsList),
new FrameworkPropertyMetadata(
defaultValue: null,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure
)
);
}
В VersionList.xaml
:
<UserControl x:Class="PcVueLauncher.Controls.VersionsList" />
...
<ListBox
...
ItemsSource="{Binding Versions}, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
SelectedItem="{Binding SelectedVersion, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
...
</UserControl>
{RelativeSource TemplatedParent}
указать привязку ссылки к элементу, к которому относится шаблон, здесь VersionsList
.
Чтобы использовать элемент управления:
<UserControl x:Class="PcVueLauncher.Views.HomeView"
...
<Controls:VersionsList
Versions="{Binding VersionsListViewModel.Versions}"
SelectedVersion="{Binding SelectedVersion}"/>
...
</UserControl>
Версии также изменены для согласования стратегии связывания.
Наконец, вы можете удалить участника VersionsListViewModel.SelectedVersion
(или использовать трюк ниже).
Что выбрать?
Со свойством зависимости элемент управления не является ссылкой на класс модели представления. Я буду использовать это для разработки библиотеки для повторного использования во многих приложениях.
С моделью представления элемент управления ожидает определенных членов в контексте данных. Я буду использовать это в прикладном решении.
Спасибо, я не видел. я поменял но все равно не работает
@StéphanF - можете ли вы отредактировать свой вопрос, чтобы добавить больше информации, как указано в комментариях к вопросу?
@StéphanF - Вы пробовали предложенное редактирование xaml из этого ответа?
В моей HomeViewModel я меняю SelectedVersion на Test1 и добавляю предложенный вами код (SelectedVersion="{Binding Test1}". Но он говорит мне, что Test1 не существует в контексте VersionListViewModel.
Я использовал решение Dependency Property, и с исправлением ошибок в предоставленном вами коде оно работает. В представлении списка я сделал это ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=VersionsItemsSource}" SelectedItem="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedVersionItem}"> Мне нужно понять почему это работает :-) Большое спасибо за помощь
Вам нужно решить несколько проблем:
Не вызывайте явно DependencyObject.CoerceValue
из обратного вызова измененного свойства. Он вызывается автоматически системой свойств зависимостей — перед обратным вызовом изменения свойства.
Ваше свойство не влияет на макет. Ради лучшей производительности не устанавливайте флаг FrameworkPropertyMetadataOptions.AffectsMeasure
, так как он будет принудительно выполнять полный проход макета каждый раз при изменении свойства, что в вашем случае не нужно. ListBox.SelectedItem
не влияет на расположение элементов управления. Вместо этого вам следует рассмотреть возможность настройки свойства для двухстороннего связывания по умолчанию, установив флаг FrameworkPropertyMetadataOptions.BindsTwoWayByDefault
.
а) Привяжите внутренние элементы Control
к свойствам элемента управления, а не к DataContext
. В противном случае ваш элемент управления становится неудобным в обращении (и в написании). Например, если вы измените DataContext
или переименуете свойства объекта, возвращаемого DataContext
, вы будете вынуждены переписать внутренние привязки для обращения к новой структуре объекта/именам свойств.
б) Это означает, что вы должны удалить все внутренние привязки DataContext
и ввести свойство зависимости для каждого. Например, удалите привязку ListBox.ItemsSource
к свойству VersionsListViewModel.Versions
и вместо этого введите, например, свойство зависимости VersionsItemsSource
.
Например, в HomeView
: определите все DataContext
связанные Binding
относительно фактического DataContext
UserControl
(или FrameworkElement
в целом) вместо того, чтобы начинать обход (используя Binding.RelativeSource
), чтобы найти родительский DataContext
, который совпадает с целью привязки DataContext
. Это бессмысленно и только показывает, что вы не поняли, как работает Binding.
Список версий.xaml.cs
public partial class VersionsList : UserControl
{
public VersionPcVue SelectedVersionItem
{
get => (VersionPcVue)GetValue(SelectedVersionItemProperty);
set => SetValue(SelectedVersionItemProperty, value);
}
public static readonly DependencyProperty SelectedVersionItemProperty = DependencyProperty.Register(
"SelectedVersionItem",
typeof(VersionPcVue),
typeof(VersionsList),
new FrameworkPropertyMetadata(default(VersionPcVue), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedVersionChanged));
public IList VersionsItemsSource
{
get => (IList)GetValue(VersionsItemsSourceProperty);
set => SetValue(VersionsItemsSourceProperty, value);
}
public static readonly DependencyProperty VersionsItemsSourceProperty = DependencyProperty.Register(
"VersionsItemsSource",
typeof(IList),
typeof(VersionsList),
new PropertyMetadata(default));
public VersionsList()
{
InitializeComponent();
}
private static void OnSelectedVersionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
}
}
VersionList.xaml
При создании Control
всегда привязывайтесь к свойствам элемента управления, а не к свойствам DataContext
:
<UserControl>
<FrameworkElement.Resources>
<ResourceDictionary>
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</ResourceDictionary>
</FrameworkElement.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Padding="10"
Text="Versions" />
<ListBox Grid.Row="1"
d:ItemsSource="{d:SampleData ItemCount=5}"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=VersionsItemsSource}"
SelectedItem="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedVersionItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="5,5,10,5"
Text="{Binding VersionName}" />
<Button
Grid.Column="1"
Padding="5"
Command="{Binding RemoveVersionCommand}"
Content="Remove"
Visibility="{Binding CanBeRemoved, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>
HomeView.xaml
Обратите внимание, что DataContext
элемента управления VersionsList
ссылается на экземпляр VersionsListViewModel
. Вы должны соответствующим образом настроить все привязки:
<UserControl>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Controls:VersionsList x:Name="test"
Grid.Column="0"
DataContext="{Binding VersionsListViewModel}"
VersionsItemsSource="{Binding Versions}"
SelectedVersionItem="{Binding SelectedVersion}" />
</Grid>
</UserControl>
"Почему я не могу вернуть выбранный элемент обратно в SelectedVersion? свойство моей модели HomeViewModel».
Учитывая ваш текущий дизайн класса и конфигурацию элемента управления, ваш элемент управления VersionsList
привязывается к свойству VersionsListViewModel.SelectedVersion
. Пока неясно, чего вы на самом деле хотите.
Либо делегируйте значение вручную, позволив HomeViewModel
прослушивать изменения свойств VersionsListViewModel.SelectedVersion
, либо удалите связанные свойства из VersionsListViewModel
и напрямую привяжите к HomeViewModel.SelectedVersion
. Класс модели представления для представления/элемента управления в большинстве случаев приводит к плохому дизайну/коду класса. Создание отдельных классов должно основываться на разных соображениях, таких как обязанности.
И тогда вы всегда хотите избежать дублирования кода (например, свойств и логики): вместо копирования вы перемещаете код в отдельные классы.
Большое спасибо @BionicCode. На самом деле, я хочу, чтобы когда я изменяю SelectedItem в UserControl, я также хочу получать уведомления в HomeViewModel с помощью свойства SelectedVersion HomeViewModel. Но, наверное, я все неправильно понял. Означает ли это, что то, что я сделал, является полной противоположностью тому, чего я хочу? Я подумал, что, как и для классического ListBox, например, в моем HomeView, и привязать SelectedItem к свойству SelectedItem ViewModel, когда выбранный элемент изменяется (нажав на него), свойство ViewModel устанавливается на это новое значение. мне еще предстоит научиться я думаю
Когда вы привязываетесь к HomeViewModel, он обновляется через привязку данных. Если вы привязываетесь к VersionsListViewModel, то HomeViewModel не обновляется через привязку данных, потому что вы не привязываетесь к нему, если это имеет смысл.
Вопрос в том, зачем вообще нужна VersionsListViewModel, если все данные нужны в HomeViewModel? Либо переместите связанные свойства из VersionsListViewModel в HomeViewModel (SelectedVersion и Versions), либо переместите логику, требующую SelectedVersion, из HomeViewModel в VersionsListViewModel. Вы должны исправить свои классы точно.
за вашу помощь – Stéphan F 5 часов назад Удалить Вы правы. Я думаю неправильно. Но в некоторых случаях возможно :-). Позвольте мне подумать об этом. Но в любом случае, большое спасибо за вашу помощь и объяснение
Если ваш код не работает, вы всегда должны публиковать полный контекст, чтобы обеспечить надлежащую проверку кода. Это очень важно. Поскольку мы не знаем вашего проекта, мы можем исправить только то, что можем проверить. Как задать хороший вопрос?.