WPF MVVM TreeView SelectedItem
Это не может быть так сложно. TreeView в WPF не позволяет вам устанавливать SelectedItem, говоря, что свойство ReadOnly. У меня есть заполнение TreeView, даже обновление при изменении коллекции данных.
Мне просто нужно знать, какой элемент выбран. Я использую MVVM, поэтому нет кода или переменной для ссылки на treeview. Это единственное решение, которое я нашел, но это очевидный взлом, он создает еще один элемент в XAML, который использует привязку ElementName, чтобы установить себя в выбранный элемент treeviews, который вы также должны привязать свою модель Viewmodel. Несколько других вопросов, но никаких других рабочих решений не задано.
Я видел этот вопрос, но используя предоставленный ответ дает компиляцию ошибок, по какой-то причине я не могу добавить ссылку на blend sdk System.Windows.Interactivity к моему проекту. В нем говорится: "Неизвестная система ошибок. Windows не была предварительно загружена", и я еще не понял, как это пройти.
Для бонусных очков: почему, черт возьми, Microsoft создала этот элемент свойства SelectedItem ReadOnly?
Ответы
Ответ 1
Вам не нужно действительно напрямую IsSelected
к свойству SelectedItem, привязать IsSelected
к свойству на вашей модели просмотра и отслеживать выбранный элемент там.
Эскиз:
<TreeView ItemsSource="{Binding TreeData}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
public class TViewModel : INotifyPropertyChanged
{
private static object _selectedItem = null;
// This is public get-only here but you could implement a public setter which
// also selects the item.
// Also this should be moved to an instance property on a VM for the whole tree,
// otherwise there will be conflicts for more than one tree.
public static object SelectedItem
{
get { return _selectedItem; }
private set
{
if (_selectedItem != value)
{
_selectedItem = value;
OnSelectedItemChanged();
}
}
}
static virtual void OnSelectedItemChanged()
{
// Raise event / do other things
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
if (_isSelected)
{
SelectedItem = this;
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Ответ 2
Вы можете создать прикрепленное свойство, которое может быть привязано и имеет getter и setter:
public class TreeViewHelper
{
private static Dictionary<DependencyObject, TreeViewSelectedItemBehavior> behaviors = new Dictionary<DependencyObject, TreeViewSelectedItemBehavior>();
public static object GetSelectedItem(DependencyObject obj)
{
return (object)obj.GetValue(SelectedItemProperty);
}
public static void SetSelectedItem(DependencyObject obj, object value)
{
obj.SetValue(SelectedItemProperty, value);
}
// Using a DependencyProperty as the backing store for SelectedItem. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(TreeViewHelper), new UIPropertyMetadata(null, SelectedItemChanged));
private static void SelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (!(obj is TreeView))
return;
if (!behaviors.ContainsKey(obj))
behaviors.Add(obj, new TreeViewSelectedItemBehavior(obj as TreeView));
TreeViewSelectedItemBehavior view = behaviors[obj];
view.ChangeSelectedItem(e.NewValue);
}
private class TreeViewSelectedItemBehavior
{
TreeView view;
public TreeViewSelectedItemBehavior(TreeView view)
{
this.view = view;
view.SelectedItemChanged += (sender, e) => SetSelectedItem(view, e.NewValue);
}
internal void ChangeSelectedItem(object p)
{
TreeViewItem item = (TreeViewItem)view.ItemContainerGenerator.ContainerFromItem(p);
item.IsSelected = true;
}
}
}
Добавьте объявление пространства имен, содержащее этот класс, в ваш XAML и привяжите его следующим образом (local - это то, как я назвал объявление пространства имен):
<TreeView ItemsSource="{Binding Path=Root.Children}"
local:TreeViewHelper.SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}"/>
Теперь вы можете привязать выбранный элемент, а также установить его в своей модели просмотра, чтобы изменить его программно, если это требование когда-либо возникнет. Это, конечно, предполагает, что вы реализуете INotifyPropertyChanged по этому конкретному свойству.
Ответ 3
Очень необычный, но довольно эффективный способ решения этой проблемы в MVVM-приемлемом способе:
- Создайте сглаженный контент ContentControl на том же самом представлении TreeView. Назовите его соответствующим образом и привяжите его содержимое к некоторому свойству
SelectedSomething
в viewmodel. Этот ContentControl будет "удерживать" выделенный объект и обрабатывать его привязку, OneWayToSource;
- Прослушайте
SelectedItemChanged
в TreeView и добавьте обработчик в коде для установки вашего ContentControl.Content в новый выбранный элемент.
XAML:
<ContentControl x:Name="SelectedItemHelper" Content="{Binding SelectedObject, Mode=OneWayToSource}" Visibility="Collapsed"/>
<TreeView ItemsSource="{Binding SomeCollection}"
SelectedItemChanged="TreeView_SelectedItemChanged">
Код за:
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
SelectedItemHelper.Content = e.NewValue;
}
ViewModel:
public object SelectedObject // Class is not actually "object"
{
get { return _selected_object; }
set
{
_selected_object = value;
RaisePropertyChanged(() => SelectedObject);
Console.WriteLine(SelectedObject);
}
}
object _selected_object;
Ответ 4
Используйте OneWayToSource
режим привязки. Это не работает. См. Править.
Изменить. Похоже, что это ошибка или "по дизайну" поведения Microsoft, согласно этому вопросу; однако есть некоторые обходные пути. Кто-нибудь из них работает для вашего TreeView?
Проблема Microsoft Connect: https://connect.microsoft.com/WPF/feedback/details/523865/read-only-dependency-properties-does-not-support-onewaytosource-bindings
Отправлено Microsoft 1/10/2010 в 2:46 вечера
Мы не можем сделать это в WPF сегодня, по той же причине, мы не можем поддерживать привязки к свойствам, которые не являются DependencyProperties. Время выполнения per-instance состояние привязки сохраняется в BindingExpression, которое мы сохраняем в EffectiveValueTable для целевого объекта DependencyObject. Если целевое свойство не является DP или DP доступно только для чтения, нет места для хранения выражения BindingExpression.
Возможно, когда-нибудь мы сможем расширить функциональность связывания к этим двум сценариям. Мы часто спрашиваем о них. В другими словами, ваш запрос уже включен в наш список функций для рассмотрите в будущих выпусках.
Спасибо за ваши отзывы.
Ответ 5
Я решил использовать комбинацию кода позади и код viewmodel. xaml выглядит так:
<TreeView
Name="tvCountries"
ItemsSource="{Binding Path=Countries}"
ItemTemplate="{StaticResource ResourceKey=countryTemplate}"
SelectedValuePath="Name"
SelectedItemChanged="tvCountries_SelectedItemChanged">
Код за
private void tvCountries_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var vm = this.FindResource("vm") as ViewModels.CoiEditorViewModel;
if (vm != null)
{
var treeItem = sender as TreeView;
vm.TreeItemSelected = treeItem.SelectedItem;
}
}
И в viewmodel есть объект TreeItemSelected, доступ к которому вы можете получить в viewmodel.
Ответ 6
Вы всегда можете создать DependencyProperty, который использует ICommand и прослушивать событие SelectedItemChanged в TreeView. Это может быть немного проще, чем привязка IsSelected, но я предполагаю, что вы все равно закроете привязку IsSelected по другим причинам. Если вы просто хотите привязываться к IsSelected, вы всегда можете отправлять свой элемент при каждом изменении IsSelected. Затем вы можете слушать эти сообщения в любом месте вашей программы.