Pull to refresh

.NET компонент — Tree View с поиском

Reading time6 min
Views12K
Когда к TreeView потребовалось сделать поиск, я сделал реализацию поиска вне контрола, а потом установил его свойство SelectedItem.
К сожалению у стандартного WPF контрола TreeView свойство SelectedItem только для чтения.
Поэтому мне пришлось ввести в каждый объект отображенный в TreeView свойства IsSelected и IsExpanded и связать эти свойства с соответствующим свойством TreeViewItem. (обычно именно это советуют в инете )
Таким образом нужно только установить у нужного объекта IsSelected, а у всех его предков IsExpanded.
Такая реализация работает прекрасно, но…
  1. Не очень красиво иметь бизнес объекту IsSelected и IsExpanded. А создавать вью модель для каждого пункта дерева муторно.
  2. Поиск по дереву должен быть реализован каждый раз в коде, что как минмум требует времени и… морока одним словом.


В общем поразмыслив немного пришел к такой концепции.
  • Поиск в объекта осуществлять по части строкового представления объекта в узле дерева.
  • На загрузке нужно пройти по всем вышестоящим HierarchicalDataTemplate и DataTemplate получить все биндинги на элементы объекта, а у HierarchicalDataTemplate путь к дочерним элементам.
  • При поиске по иерархической коллекции Item Source. Использовать эти данные для поиска элемента.
  • Все найденные элементы сохранять вместе путем к узлу.
  • Когда надо выделить один из найденных элементов нужно просто выделить нужный узел по запомненному пути.
  • Если нужно просто выделить объект (реализация двунаправленного свойства SelectedItem), то нужно найти его в иерархической ItemSource, запомнить путь к нему и потом выделить нужный узел по пути.


Вот так я осуществил поиск во всех шаблонах. Тут все относительно просто.
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            templateDescrColl = new TemplateDescriptionCollection();//создал коллекцию описаний биндингов
List<FrameworkElement> parents=   Helper.GetAllParent(this);// поулучил спискок предков
FindDataTemplatesResources(this.Resources);// поулучил описания шаблонов описанные ресурсах 
              foreach (FrameworkElement parent in parents) // потом во всех предках
            {
                FindDataTemplatesResources(parent.Resources);
            }
            FindDataTemplatesResources(System.Windows.Application.Current.Resources);// и под конец в приложении
        }

Вот логика поиска шаблонов, здесь все как в задуманно за исключением того, что предусмотрен альтернанивный способ задания путей поиска.
Это может быть необходимо с одной стороны потому, что не всегда срабатывает поиск биндингов.
(в случае когда для показа узла используется отдельный контрол со своей достаточно сложной логикой точно не работает).
Ну а с другой стороны иногда надо более точно указывать путь для поиска.
Путь задается через свойство FindPatches в виде строки «TypeName1:Property1,Property2 TypeName2:»
В данном случае для типа TypeName1 будет осуществлен поиск по свойствам Property1,Property2, а по TypeName2 поиск вообще не будет осуществлен.
        private void FindDataTemplatesResources(ResourceDictionary Resource)
        {
            foreach (object xxx in Resource.Values)
            {                
                DataTemplate dataTemplate = xxx as DataTemplate;
                if (dataTemplate != null)// если a DataTemplate
                {
                    bool hierarhical=false;
                    if (!templateDescrColl.ExistType(dataTemplate.DataType as Type, out hierarhical))// если колекция нужного типа
                    {
                        TemplateBindingDescr templateBindingDescr = new TemplateBindingDescr();//
                        HierarchicalDataTemplate hdt = xxx as HierarchicalDataTemplate;// если  HierarchicalDataTemplate here
                        if (hdt != null)
                        {
                            Binding ItemsSourceBinding = hdt.ItemsSource as Binding;
                            string ItemsSourcePath = ItemsSourceBinding.Path.Path;
                            templateBindingDescr.itemSourcePath = ItemsSourcePath;// путь для поиска во внутренней коллекции
                            templateBindingDescr.IsHierathical = true;
                        }                    
                         Type tType = dataTemplate.DataType as Type;
                        templateBindingDescr.TargetType = tType; // type шаблона
                        if (! String.IsNullOrWhiteSpace (FindPatches) && FindPatches.Contains (tType.Name + ":")) //если задан альтернативный поиск для этого типа
                        {
                            Match match = Regex.Match (FindPatches, tType.Name + ":([^:]*)");.
                            if (match.Success)
                            {
                                string re = match.Groups [1]. Value;
                                string [] pathes = re.Split (',');
                                templateBindingDescr.BindingPathes.AddRange (pathes);
                            }
                         }
                        else //Визуализирую шаблон и ищу все его биндинги
                        {
                            FrameworkElement frameworkElement = dataTemplate.LoadContent () as FrameworkElement; // загрузил шаблон
                            List <FrameworkElement> DependencyObjects = Helper.GetAllVisualChildren (frameworkElement);//нашел в нем все элементы
                            if (DependencyObjects!= null)
                            {
                                foreach (FrameworkElement dependencyObject in DependencyObjects)
                                {
                                    //получил биндинги на текст
                                    BindingExpression BE = System.Windows.Data.BindingOperations.GetBindingExpression (dependencyObject, TextBlock.TextProperty);
                                    if (BE!= null)
                                    {
                                         string path = BE.ParentBinding.Path.Path;
                                        templateBindingDescr.BindingPathes.Add (path);
                                    }
                                }
                            }
                        }
                        templateDescrColl.Add (templateBindingDescr); // добавил в коллекцию
                    }
                }
            }
        }

Вот так выглядит поиск всех элементов содержащих искомый текст.
        /// <summary>
        /// Иерархический поиск всех узлов в методом поиска по указанным путям  если в получившимся объекте после приведения его к строке есть FindText
        /// Работает гораздо быстрее 
        /// </summary>
        /// <param name="control"></param>
        /// <param name="FindText"></param>
        /// <returns></returns>
        public List<NamedObject> FindObjectByPropertyNames(IEnumerable itemsSource, List<int> path)
        {
            List<NamedObject> ret = new List<NamedObject>();//Создали список куда будем епомещать результаты поиска
            int i = 0;
            path.Add(i);//добавили в путь этот уровень иерархии
            foreach (object Item in itemsSource) //Перебираем все элементы коллекции
            {
                path[path.Count - 1] = i;//по ходу дела устанавливаем текущий путь

                TemplateBindingDescr desctiption= templateDescrColl.Get(Item.GetType());//ищем в коллекции описаний подходящий по типу шаблон
                string subItemsPath=desctiption.itemSourcePath;//получили путь по которому искать детей

                string ObjectText=String.Empty;//здесь мы будем хранить текст для представления объекта в комбобоксе поиска 
                bool ok=false;
                foreach (string findPath in desctiption.BindingPathes)// прошлись по путям указанным в биндинге данного узла
                {
                    object ItemVal = Item.GetObjectSubItem(findPath); //Получили по пути объект
                    if (ItemVal != null )//Если он не нулл
                    {
                        string ItemText = ItemVal.ToString().ToUpper();//получаем текстовое представление объекта
                        ObjectText += ItemText + " ";
                        if (ItemText.Contains(FindText.ToUpper())) 
                        {
                            ok = true; //объект содержит искомую строку пометим его
                        }
                    }
                 }

                if (ok)//если объект содержал найденные значения
                {
                    List<int> path2 = new List<int>();
                    path2.AddRange(path); 
                    ret.Add(new NamedObject() { Name = ObjectText, Item = Item, Path = path2 });// поместим его в коллекцию
                }
                if (subItemsPath != null)//А теперь пройдем по его детям
                {
                    IEnumerable  subItems=Item.GetObjectSubItem(subItemsPath) as IEnumerable;
                    if (subItems != null)
                    {
                        List<int> path1 = new List<int>();
                        path1.AddRange(path);
                        ret.AddRange(FindObjectByPropertyNames(subItems, path1));
                    }
                }
                i++;
            }
            return ret;
        }


Вот так выглядит поиск всех элементов содержащих искомый текст.
int failCount; //количество падений поиска 
        //выделение узла по пути 
        void SelectNodeByPath(ItemsControl control, IEnumerable<int> path)
        {
            List<int>L= path.ToList<int>();
            if (L.Count == 0) { return; }
             if (control.Items.Count > L[0])                                
            {
                 do   //тут вот чуть подождем пока сонтрол сгенерит своих детей
                {
                    control.UpdateLayout();
                }
                while (control.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated);

                TreeViewItem dObject = control.ItemContainerGenerator.ContainerFromIndex(L[0]) as TreeViewItem;
                if (dObject == null)//не нашли сразу применим специально обученный метод
                {
                    dObject = GetTreeViewItem(control, L [0]);
                }
                if (dObject == null)
                {

                       MessageBox.Show("Не могу найти  );
              
                }
                else  //нашли родимого
                {
                    L.RemoveAt(0);
                    if (L.Count == 0) //последнее звено в дереве  его нуждно выделить
                    {
                        dObject.IsSelected = true;
                        dObject.BringIntoView();
                    }
                    else  //Это только промежуточный узел и его надо раскрыть 
                    {
                        dObject.IsExpanded = true;
                        SelectNodeByPath(dObject, L); //и искать дальше
                    }
                }
            }
            else
            {
                MessageBox.Show("В контроле"+control.Items.Count+" Задан поиск > "+L[0]);
            }
        }


Функцию GetTreeViewItem(control, L [0]); позволяющую надежно найти узел сделал благодаря
Deranged спасибо ему большое. На основе кода с указанного им сайта.
blogs.msdn.com/b/wpfsdk/archive/2010/02/23/finding-an-object-treeviewitem.aspx
Вот здесь новый код.
myTreeViewWith search.zip
Поиск работает в отдельном потоке.

Возможно сделать расширенный поиск что бы пользователь мог выбирать свойства по которым искать
и… Но это уже украшательства идея вот и работает.
P.S Автор поста, мой брат SergejSh — все вопросы и замечания к нему.
Tags:
Hubs:
Total votes 37: ↑24 and ↓13+11
Comments35

Articles