WPF MVVM TreeView SelectedItem

时间:2011-08-22 21:27:13

标签: wpf mvvm treeview

这不可能是这么困难。 WPF中的TreeView不允许您设置SelectedItem,表示该属性是ReadOnly。我有TreeView填充,甚至在数据绑定集合更改时更新。

我只需知道选择了哪个项目。我正在使用MVVM,因此没有代码隐藏或变量来引用树视图。 This is the only solution我找到了,但这是一个明显的黑客攻击,它在XAML中创建了另一个元素,它使用ElementName绑定将自己设置为树视图选择项,然后你必须绑定你的Viewmodel。 Several其他questions被问及这个问题,但没有给出其他工作解决方案。

我见过this question,但是使用给出的答案给出了编译错误,由于某种原因我无法在我的项目中添加对sdk System.Windows.Interactivity的混合引用。它说“未知的错误系统。窗口没有被预加载”,我还没有想出如何超越它。

对于奖励积分:为什么微软会让这个元素的SelectedItem属性ReadOnly?

6 个答案:

答案 0 :(得分:46)

您不应该直接处理SelectedItem属性,将IsSelected绑定到viewmodel上的属性并跟踪所选项目。

草图:

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

答案 1 :(得分:12)

您可以创建一个可绑定且具有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并按如下方式绑定(本地是我命名名称空间声明的方式):

<TreeView ItemsSource="{Binding Path=Root.Children}"
          local:TreeViewHelper.SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}"/>

现在,您可以绑定所选项目,并在视图模型中将其设置为以编程方式更改它,如果该要求出现的话。当然,这是假设您在该特定属性上实现INotifyPropertyChanged。

答案 2 :(得分:6)

以MVVM可接受的方式解决此问题的一种非常不寻常但非常有效的方法如下:

  1. 在TreeView所在的View上创建一个visibility-collapsed ContentControl。适当地命名,并将其内容绑定到viewmodel中的某个SelectedSomething属性。这个ContentControl将会#34;持有&#34;选定的对象并处理它的绑定,OneWayToSource;
  2. 在TreeView中收听SelectedItemChanged,并在代码隐藏中添加处理程序,将ContentControl.Content设置为新选择的项目。
  3. 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;
        }
    

    视图模型:

        public object SelectedObject  // Class is not actually "object"
        {
            get { return _selected_object; }
            set
            {
                _selected_object = value;
                RaisePropertyChanged(() => SelectedObject);
                Console.WriteLine(SelectedObject);
            }
        }
        object _selected_object;
    

答案 3 :(得分:4)

使用OneWayToSource绑定模式。这不起作用。见编辑。

编辑:根据this question,看起来这是微软的错误或“按设计”行为;但是,有一些解决方法。这些是否适合您的TreeView?

Microsoft Connect问题:https://connect.microsoft.com/WPF/feedback/details/523865/read-only-dependency-properties-does-not-support-onewaytosource-bindings

  

Microsoft于2010年1月10日下午2:46发布

     

我们今天无法在WPF中执行此操作,原因与我们无法支持的原因相同   对不是DependencyProperties的属性的绑定。运行时   绑定的每个实例状态保存在BindingExpression中   我们存储在目标DependencyObject的EffectiveValueTable中。   当目标属性不是DP或DP是只读时,就是   没有地方存储BindingExpression。

     

我们可能有一天会选择扩展绑定功能   这两种情况。我们经常被问到这些问题。在   换句话说,您的请求已经在我们的功能列表中   在将来的版本中考虑。

     

感谢您的反馈。

答案 4 :(得分:2)

我决定使用代码隐藏和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中访问该对象。

答案 5 :(得分:1)

您始终可以创建使用ICommand的DependencyProperty并在TreeView上侦听SelectedItemChanged事件。这可能比绑定IsSelected有点容易,但我想你会因为其他原因而最终结束IsSelected。如果您只想在IsSelected上绑定,只要IsSelected发生更改,您就可以始终让项目发送消息。然后,您可以在程序中的任何位置收听这些消息。