c# - WPF - 使用嵌套的 HierarchicalDataTemplate 在 TreeView 中刷新 CollectionView 而不会丢失选择

标签 c# wpf xaml treeview

为什么这这么难做到,说真的:P我有以下内容:

 public BindingList<Dialogue> Dialogues { get; set; }
 public CollectionView DialoguesCollectionView { get; set; }

在此 ViewModel 的构造函数中,我执行以下操作:

SubDialogues = new BindingList<SubDialogue>();
SubDialogues.ListChanged += (sender, args) =>
{
    OnPropertyChanged(nameof(SubDialogues));

    //HACK: Temporary solution...
    if (args.ListChangedType == ListChangedType.ItemAdded ||
        args.ListChangedType == ListChangedType.ItemDeleted)
    {
        SubDialoguesCollectionView.Refresh();
    }
};

SubDialoguesCollectionView = new ListCollectionView(SubDialogues);
SubDialoguesCollectionView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));

每当我修改绑定(bind)列表中的项目时,它都会立即反射(reflect)出来。但是,这不适用于添加或删除项目,因此我强制刷新。

但是,我还想在 Name 属性发生更改时刷新列表,因此它会相应地重新排序,这就是我的问题开始的地方。

我的 View 中的 TreeView 使用 SubDialoguesCollectionView 作为其 HierarchicalDataTemplate 之一的 ItemSource。每当我在 TreeView 中选择一个项目并且底层 CollectionView 由于某种原因而刷新时,它就会丢失选择并跳转到整个列表的父级。

我不希望发生这种情况,我想保留项目的选择,即使其偏移量由于重新排序而发生变化。谁能帮助我弄清楚如何让它按照我想要的方式工作?我确实尝试了一切:SortableBindingList、行为、移动项目而不是刷新,列表还在继续。我开始长出白发了! :P

如果还有其他我可能忽略的方法,我也很想听听。我最终想要的只是一个列表,当任何绑定(bind)对象的 Name 属性发生更改时,该列表会自行重新排序,但不会丢失 TreeView 中对该对象的选择。


根据要求,复制/粘贴我的代码库的设置方式(稍微简化和扁平化):https://gist.github.com/LennardF1989/59a42c7be474061f14bd

我遗漏了上面描述的失败尝试,这是我尝试任何操作之前它所处的状态。

最佳答案

这里实际上存在几个问题。老实说,主要问题是您没有正确使用数据绑定(bind),如果您使用 MVVM 之类的东西,那么您将操纵 View 模型,并且 UI 将陷入困境。实际上, View 元素内部发生了一些复杂的交互。

在进一步讨论之前,我只想指出我对您的代码所做的一些更改。

  • 不要使用BindingList,它根本不能很好地扩展;我已将您的替换为 ObservableCollections。
  • 您已经导入了 MVVM Lite,因此您也可以使用 ViewModelBase 而不是声明您自己的 BaseViewModel。当然,由于 C# 不支持多重继承,您仍然会遇到 MainWindow 问题,但无论如何,MainWindow 中不应该有任何代码;)
  • 为了引导代码实现更好的数据绑定(bind),我用附加行为替换了更新 SelectedObject 的函数,源代码如下。 (这与问题无关,所以我希望你能原谅我这一点)。
  • 我创建了第二个字段,名为 SortedDialogues,它是对话的 CollectionViewSource。我还添加了一个 SortDescription 以按“名称”排序。因此,对话可以单独保留,您可以绑定(bind)到 SortedDialogues,每当某些内容(即对话名称)发生更改时,您可以调用 SortedDialogues' Refresh()成员(member)更新排序。
  • 我只在对话中这样做,我会让你将其应用到任务等中。

现在,TreeView 维护了一个 TreeViewItems 的内部列表,每个列表都有一个 IsSelected标志(在 MVVM 应用程序中,您可以将它们绑定(bind)到相应 View 模型中的属性)。当您刷新排序列表时,TreeView 会通过从头开始为相关子树重新创建 TreeViewItems 进行响应。当前选定的 TreeViewItem 与 TreeView 分离,因此框架通过选择父节点(即 Quest)进行响应。一般情况下可以调用treeview.ItemContainerGenerator.ContainerFromItem(item)从项目中获取新的 TreeView,但这不适用于 HierarchicalDataTemplates,因为在运行时创建节点的动态方式。诀窍是手动遍历 TreeView 的层次结构,查找具有所选值的节点。

这是新代码:

public interface INameable
{
    string Name { get; set; }
}

public class Quest : ViewModelBase, INameable
{
    private string _name;

    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;

            RaisePropertyChanged();
        }
    }

    private ObservableCollection<Dialogue> _Dialogues;
    public ObservableCollection<Dialogue> Dialogues
    {
        get { return this._Dialogues; }
        set { this._Dialogues = value; RaisePropertyChanged(); }
    }

    private ICollectionView _SortedDialogues;
    public ICollectionView SortedDialogues
    {
        get { return this._SortedDialogues; }
        set { this._SortedDialogues = value; RaisePropertyChanged(); }
    }

    // this is actually a bit messy, I'll leave it to you to find a cleaner way of doing this
    public Func<object> GetSelected;
    public Action<object> SetSelected;

    public Quest()
    {
        Dialogues = new ObservableCollection<Dialogue>();

        this.SortedDialogues = CollectionViewSource.GetDefaultView(Dialogues);
        this.SortedDialogues.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
    }

    public void AddDialogue(Dialogue dlg)
    {
        this.Dialogues.Add(dlg);
        dlg.PropertyChanged += Dlg_PropertyChanged;
        SortDialogues();
    }

    private void Dlg_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        SortDialogues();
    }

    private void SortDialogues()
    {
        var selected = GetSelected(); // get currently selected item
        this.SortedDialogues.Refresh(); // bam! treeviewitmes get destroyed.
        SetSelected(selected); // so reselect it immediately
    }
}

public class Dialogue : ViewModelBase, INameable
{
    private string _name;

    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            RaisePropertyChanged();
        }
    }

}

public partial class MainWindow : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;


    private object _selectedObject = new object();
    public object SelectedObject
    {
        get
        {
            return _selectedObject;
        }
        set
        {
            _selectedObject = value;

            OnPropertyChanged();
        }
    }

    public ObservableCollection<Quest> Quests
    {
        get { return _quests; }
        set
        {
            _quests = value;
            OnPropertyChanged();
        }
    }

    private ObservableCollection<Quest> _quests;        

    public MainWindow()
    {
        Quests = new ObservableCollection<Quest>();

        InitializeComponent();
    }

    private void AddQuest(object sender, RoutedEventArgs e)
    {
        Quests.Add(new Quest
        {
            Name = "Quest",
            GetSelected = () => this.SelectedObject,
            SetSelected = (selected) => {
                ItemContainerGenerator gen = _treeView.ItemContainerGenerator;
                TreeViewItem item = ContainerFromItem(gen, selected);
                if (item != null)
                    item.IsSelected = true;
            }
            ,
        });
    }

    private void AddDialogue(object sender, RoutedEventArgs e)
    {
        if (_treeView.SelectedItem is Quest)
        {
            var dlg = new Dialogue
            {
                Name = "Dialogue"
            };
            (_treeView.SelectedItem as Quest).AddDialogue(dlg);
        }
    }

    // courtesy http://stackoverflow.com/questions/24859511/get-treeviewitem-for-treeview-logical-element
    private static TreeViewItem ContainerFromItem(ItemContainerGenerator containerGenerator, object item)
    {
        TreeViewItem container = (TreeViewItem)containerGenerator.ContainerFromItem(item);
        if (container != null)
            return container;

        foreach (object childItem in containerGenerator.Items)
        {
            TreeViewItem parent = containerGenerator.ContainerFromItem(childItem) as TreeViewItem;
            if (parent == null)
                continue;

            container = parent.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
            if (container != null)
                return container;

            container = ContainerFromItem(parent.ItemContainerGenerator, item);
            if (container != null)
                return container;
        }
        return null;
    }

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

}

这是 TreeView 的 XAML:

    <TreeView x:Name="_treeView" behaviors:TreeViewHelper.SelectedItem="{Binding Path=SelectedObject, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">

        <TreeViewItem IsExpanded="True">

            <TreeViewItem.Header>
                <TextBlock Text="Project" Margin="5,0,0,0" />
            </TreeViewItem.Header>
            <TreeViewItem ItemsSource="{Binding Quests}" IsExpanded="True">
                <TreeViewItem.Header>
                    <TextBlock Text="Quests" Margin="5,0,0,0" />
                </TreeViewItem.Header>
                <TreeViewItem.Resources>
                    <HierarchicalDataTemplate DataType="{x:Type testApplication:Quest}" ItemsSource="{Binding SortedDialogues, UpdateSourceTrigger=PropertyChanged}">
                        <TextBlock Text="{Binding Name}" Margin="5,0,0,0" />
                    </HierarchicalDataTemplate>
                    <DataTemplate DataType="{x:Type testApplication:Dialogue}">
                        <TextBlock Text="{Binding Name}" Margin="5,0,0,0" />
                    </DataTemplate>
                </TreeViewItem.Resources>
            </TreeViewItem>
        </TreeViewItem>

    </TreeView>

最后是绑定(bind)当前所选项目的行为,它在网络上以各种形式存在,所以我不知道它最初来自哪里:

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 will be null for HierarchicalDataTemplates
            if (item != null)
                item.IsSelected = true;
        }
    }
}

关于c# - WPF - 使用嵌套的 HierarchicalDataTemplate 在 TreeView 中刷新 CollectionView 而不会丢失选择,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34957565/

相关文章:

c# - 为列表框中的每个项目加载不同的数据模板

c# - 如何在 WPF 中创建一个带有绑定(bind)的验证文本框,在无效输入后切换回最后一个有效值?

c# - 右对齐 TextBlock,TextWrapping 在左侧环绕

wpf - Datagrid将wpf c#绑定(bind)到datatable.defaultview : Last column of data missing but header is there

c# - 如果可以,我可以在 web.config 或 app.config 中设置条件编译常量吗?

c# - 真正的模态窗口可能吗?

xaml - 在 Windows 8.1 存储 XAML 中添加新项目后,ListView.ContainerFromItem 返回 null

c# - 获取 DropDownList 的选定值。 ASP.NET MVC

c# - 如何启动 WCF 语音聊天应用程序?

c# - 可以从 IronPython 使用 scikit 学习吗?