Hierarchiczne TreeView z listy obiektów

Wielokrotnie stawałem przed problemem zbudowania hierarchicznego TreeView z listy obiektów.
Pisanie odpowiednich foreach, budowanie całej hierarchii TreeNode’ów stało się za którymś razem męczące.

Parę dni temu doczytałem o TreeNodeBindings i postanowiłem chociaż w części zautomatyzować to zadanie. Najpierw przyszło mi na myśl bezpośrednie podpięcie listy do DataSource całego TreeView, jednak jak szybko się okazało kolekcja taka wymaga implementacji interfejsu IHierarchicalEnumerable a jej elementy dodatkowo IHierarchyData.

Bardzo szybko zapadła więc decyzja – nie tędy droga ;-) Chciałem rozwiązania dość uniwersalnego, które nie będzie wymagało dodatkowych działań w klasach.
Jak więc „oszukać” i podłączyć jako DataSource obiekt, który automatycznie odwali za nas częśc pracy? Więc odpowiedź okazała się dość banalna – XML.

Wystarczy zbudować w pamieci obiekt XML, składający się wyłącznie z tych danych, które chcemy pokazywać na drzewie i właśnie jego podłączyć jako DataSource, używając odpowiednio ustawionego TreeNodeBindings.

Pokażę więc poniżej jak to zrealizować.

Na początek prosta klasa, która posłuży do dalszych demonstracji:

 public class OrgUnit
    {
        private int _id;

        public int Id
        {
            get { return _id; }
            set { _id = value; }
        }

        private int _parentId;

        public int ParentId
        {
            get { return _parentId; }
            set { _parentId = value; }
        }

        private string _name;

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

        private string _description;

        public string Description
        {
            get { return _description; }
            set { _description = value; }
        }
}

Jak widać, posiada ona property ParentId – właśnie ono będzie odpowiedzialne za budowanie hierarchii.

Na potrzeby tego przykładu zbudujmy sztuczną listę, którą załadujemy nasze drzewo:

List<OrgUnit> list = new List<OrgUnit>();

list.Add(new XmlTest.OrgUnit(11, -1, "Firma X", ""));
list.Add(new XmlTest.OrgUnit(1, 0, "Dział produkcji", "Dział produkcji zajmuje się..."));
list.Add(new XmlTest.OrgUnit(9, 0, "Dział handlowy", "Dział handlowy zajmuje się..."));
list.Add(new XmlTest.OrgUnit(4, 9, "Dział zamówień", "Dział zamówień zajmuje się..."));
list.Add(new XmlTest.OrgUnit(2, 1, "Dział logistyki", "Dział logistyki zajmuje się..."));
list.Add(new XmlTest.OrgUnit(3, 0, "Dział księgowości", "Dział księgowości zajmuje się..."));
list.Add(new XmlTest.OrgUnit(6, 1, "Dział wysyłki", "Dział wysyłki zajmuje się..."));

list.Sort(
      delegate(OrgUnit o1, OrgUnit o2)
      {
         return o1.ParentId.CompareTo(o2.ParentId);
      }
      );

Bardzo ważne jest, aby lista była posortowana malejąco według property, któe tworzy hierarchię. Tutaj zrealizowane jest to przy użyciu delegata, ale ładując dane z bazy można po prostu posortować je w procedurze SQL.

Zwróćcie również uwagę, że na początku dodany jest sztuczny element z id równym -1. Ten zabieg jest niestety niezbędny, gdyż dokument XML może zawierać tylko jeden główny element. Najpierw więc dodamy element „Firma X”, który będzie wewnętrz zawierał wszystkie pozostałe. Żaden z pozostałych elementów listy nie wskazuje jako parentId tego głównego (-1). Jest to swoisty sztuczny element, który zdecydowanie ułatwi robotę.

Pierwszym krokiem będzie więc zbudowanie dokumentu XML w pamięci:

XmlDocument xmlDoc = new XmlDocument();
XmlDeclaration xmlDeclaration = xmlDoc.CreateXmlDeclaration("1.0", null, null);
xmlDoc.AppendChild(xmlDeclaration);

Następnie w pętli iterujemy po wszyskich obiektach listy, tworząc odpowiednie elementy xml.

foreach (OrgUnit item in list)
{
        XmlElement node = xmlDoc.CreateElement("_" + item.Id);
        node.SetAttribute("Id", item.Id.ToString());
        node.SetAttribute("ParentId", item.ParentId.ToString());
        node.SetAttribute("Name", item.Name);
        node.SetAttribute("Description", item.Description);
        ...
        ...
}

Teraz najważniejszy element, od którego zależy poprawne wyświetlanie ostatecznych danych.
„Algorytm” dodawania wygląda następująco:

  • Jeśli element ma Id == -1 dodajemy go jako główny do dokumentu
  • Jeśli element ma Id == 0 dodajemy go jako drugi w hierarchii element, czyli do elementu z pkt. 1
  • Jeśli element ma Id > 0 odnajdujemy element, którego Id == ParentId obecnego i wstawiamy do niego

Kod:

   if (item.ParentId == -1)
      xmlDoc.AppendChild(node);
   else if (item.ParentId == 0)
      xmlDoc.ChildNodes[1].AppendChild(node);
   else
   {
      string searchExpression = String.Format("//*[@Id=\"{0}\"] ", item.ParentId);
      XmlNode parent = xmlDoc.SelectSingleNode(searchExpression);

      if (parent != null)
      {
         parent.AppendChild(node);
      }
      else
      {
      throw new Exception("Parent node not found - invalid data");
      }

Dość istotne jest odpowiednie użycie xpath do wyszukiwania, w postaci:

String.Format("//*[@Id=\"{0}\"] ", item.ParentId)

Pozwala to na łatwe odszukanie rodzica dla obecnie rozpatrywanego elementu. Oczywiście niezbędne jest sprawdzenie, czy udało się odnaleźć rodzica (!= null). W przeciwnym przypadku ja zdecydowałem się na rzucenie wyjąktu.

Mamy teraz w pamięci dokument XML, zbudujmy więc z niego XmlDataSource, które jest akceptowalne przez TreeView.

W tym celu tworzymy nową instancję klasy XmlDataSource, nadajemy jej unikalne ID i jako property Data wpisujemy nasz stworzony dokument XML:

            XmlDataSource dataSource = new XmlDataSource();
            dataSource.ID = Guid.NewGuid().ToString();
            dataSource.Data = doc.OuterXml;

Teraz wystarczy już tylko poinformować nasze TreeView, których atrybutów XML ma używać do generowania pojedynczego TreeNode’a. W tym celu stworzyć należy obiekt TreeNodeBinding:

            TreeNodeBinding binding = new TreeNodeBinding();
            binding.TextField = "Name";
            binding.ToolTipField = "Description";
            binding.ValueField = "Id";

Na samym końcu podpinamy wszystko do naszej instancji TreeView i cieszymy się hierarchiczną strukturą ;-)

treeView1.DataBindings.Add(binding);
treeView.DataSource = dataSource;
treeView.DataBind();

Jeśli po przeczytaniu masz wrażenie, że jest tutaj więcej pracy niż przy zwykłym budowaniu pojedynczych TreeNode’ów i dodawaniu ich ręcznie do kolekcji, to zastanów się, czy przy takim ręcznym generowaniu struktury użyjesz gdzieś drugi raz tego samego kodu?

Rozwązanie, które przedstawiam powyżej śmiało można po niewielkich modyfikacjach dołączyć do prywatnych klas pomocniczych / frameworka i używać go z listami bardzo różnych obiektów. Wystarczy, aby miały one Id oraz ParentId (nawet niekoniecznie tak się nazywające).

Wystarczyłoby stworzyć uniwersalną metodę budującą XML z podanych properties klasy, pobierając ich wartości przy użyciu Reflection. Mam zamiar to zrobić jak tylko znajdę chwilkę – wtedy zamieszczę odpowiedni kod w oddzielnym wpisie.

Tymaczem poniżej możecie pobrać w pełni działające źródło do tego artykułu:


Podobne wpisy
Własny validator w ASP.NET – CheckBoxListValidator

Możesz śledzić odpowiedzi do tego wpisu za pomocą RSS 2.0 feed. Możesz leave a response, or trackback z Twojej własne strony.

Komentarze: 2 »

 
 

Dodaj komentarz

XHTML: Możesz użyć następujących tagów: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

*