czwartek, 7 października 2010

DirectoryTreeView - nowe funkcje

Do kontrolki którą stworzyłem w poprzednim poście dodałem nowe funkcjonalności, które wcześniej zaplanowałem, a mianowicie wystawianie na zewnątrz kontrolki aktualnie wybranego katalogu, możliwość wskazania zadanego katalogu oraz ukrywanie niedostępnych katalogów.

Aktualnie wybrany katalog
Jest to miejsce, w którym moja aktualna wiedza zmusza mnie do złamania zasada wzorca MVVM. Chciałem mieć możliwość bindowania aktualnego katalogu do innych kontrolek. Nie udało mi się udostępnić odpowiedniego Dependency Property w obiekcie DirectoryTreeViewModel, ponieważ nie dziedziczy ona po DependencyObject, dlatego nie udostępnia metod GetValue oraz SetValue potrzebnych do implementacji. Dlatego storzyłem DependencyProperty typu string w CodeBehind kontrolki, to znaczy w pliku DirectoryTreeView.xaml.cs:
public string CurrentDirectory
{
 get { return (string)GetValue(CurrentDirectoryProperty); }
 set { SetValue(CurrentDirectoryProperty, value); }
}

public static readonly DependencyProperty CurrentDirectoryProperty =
 DependencyProperty.Register("CurrentDirectory", typeof(string), typeof(DirectoryTreeView), new UIPropertyMetadata(""));
Natomiast aktualizację aktualnie wybranego folderu zrealizowałem poprzez wykorzystanie mechanizmu komunikatów z MVMM Light Toolkit. W obiekcie DirectoryItemViewModel, który przypisany jest jako DataContext dla każdego obiektu TreeViemItem w drzewku katalogów, podczas aktualizacji właściwości IsSelected na wartość true zostaje wysłany komunikat o zmianę wartości własności. Nie tworzę własnego typu komunikatów, jedynie wysyłam komunikat typu DirectotyItem z tokenem, którym jest po prostu string zawierający treść "SelectedDirectoryItemChangedToken". Abym nie musiał w różnych miejscach wpisywać tego ciągu znaków z palca, umieściłem go w zasobach projektu. Całość sprowadza się do dodania we właściwości IsSelected, w sekcji set następującego kodu:
if (_isSelected)
 Messenger.Default.Send<DirectoryItem>(_fileitem, 
  Chupacabra.Controls.Properties.Resources.SelectedDirectoryItemChangedToken);
Teraz należy gdzieś odebrać wysłany komunikat i przypisać go do Dependency Property. I tu niestety następuje pogwałcenie zasad MVVM. Kod odpowiedzialny za to przechwycenie umieszczam w Code Behind kontrolki, a dokładniej w konstruktorze:
Messenger.Default.Register<DirectoryItem>(this, Properties.Resources.SelectedDirectoryItemChangedToken,
 item =>
 {
  this.CurrentDirectory = item.Path;
 });
Teraz możliwe jest zbindowanie właściwości CurrentDirectory do dowolnych elementów z poziomu XAML'a. Na przykład można do projektu dodać pole tekstowe na bieżąco pokazujące wybrany folder:
<my:DirectoryTreeView Name="directoryTreeView"/>
<TextBlock Text="{Binding CurrentDirectory, ElementName=directoryTreeView}" />

Rozwijanie drzewka do wybranego katalogu
Wydaje mi się, że ta opcja jest konieczna w tego typu kontrolce. Chodzi o to aby móc rozkazać kontrolce rozwinąć drzewo katalogów do określonego folderu i na koniec go zaznaczyć jako aktualny. Aby wykonać tę operację należy za pomocą obiektu Messenger wysłać komunikat nowego typu stworzonego specjalnie na potrzeby kontrolki:
public class OpenPathMessage : MessageBase
{
 public string Path { get; set; }

 public OpenPathMessage(string _path)
 {
  Path = _path;
 }
}
Jedyna specyficzna informacja do przekazania to pełna ścieżka do folderu jaki chcemy otworzyć. Można to osiągnąć na przykład następującą linijką:
Messenger.Default.Send<Controls.Messages.OpenPathMessage>(
 new Controls.Messages.OpenPathMessage(@"D:\Programy\WTW"));
W konstruktorze DirectoryTreeViewModel można odebrać ten komunikat przez:
Messenger.Default.Register<Messages.OpenPathMessage>(this, openPath);
W funkcji openPath należy przejść przez wszystkie katalogi na jakie składa się pełna ścieżka, ustawić dla każdego elementu właściwość IsExpanded na true, żeby ewentualnie wczytać listę katalogów. 
private void openPath(Chupacabra.Controls.Messages.OpenPathMessage m)
{
 string path = m.Path;
 string[] split = path.Split(new char[] { '\\' });
 var divm = findRootViewModel(split[0]);
 int i = 1;

 while (divm != null && i < split.Length)
 {
  divm.IsExpanded = true;
  divm = divm.FindViewModel(split[i++]);
  if (divm.Path == path)
   divm.IsSelected = true;
 }
}
Najpierw rozdzielam ścieżkę na poszczególne składowe i sprawdzam czy litera dysku (czyli pierwszy element z tablicy) znajduje się na liście, jeśli tak to zwracam odpowiadający mu obiekt ViewModel:
private DirectoryItemViewModel findRootViewModel(string name)
{
 foreach (var item in _root)
 {
  if (item.Name.Trim('\\') == name)
   return item;
 }
 return null;
}
Następnie iteruje przez pozostałe w ścieżce elementy i rozwijam kolejne gałęzie. Jeśli uda mi się osiągnąć zadaną ścieżkę to dodatkowo zaznaczam końcowy element jako aktualny. Funkcja zaimplementowana w DirectoryItemViewModel pozwalająca przeszukać wszystkich potomków:
public DirectoryItemViewModel FindViewModel(string path)
{
 foreach (var item in _children)
 {
  if (item._fileitem.Name.Trim('\\') == path)
   return item;
 }
 return null;
}
Pomijanie ukrytych folderów
Tak naprawdę jest to bardzo prosta funkcjonalność. Jedyne czego wymagała to zmienienia ciała funkcji loadChildren z DirectoryItemViewModel:
private void loadChildren()
{
 foreach (var s in Directory.GetDirectories(_fileitem.Path))
 {
  DirectoryInfo di = new DirectoryInfo(s);
  if ((di.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden)
  {

   DirectoryItem item = new DirectoryItem
   {
    Name = s.Substring(s.LastIndexOf(@"\") + 1),
    Path = s
   };
   _children.Add(new DirectoryItemViewModel(item));
  }
 }
} 

Zaktualizowane źródła można znaleźć w standardowym miejscu.

Brak komentarzy:

Prześlij komentarz