niedziela, 14 listopada 2010

“Daj się poznać” – zakończenie :/

15 tygodni konkursu minęło. Czas na małe podsumowanie prac i trochę przemyśleń. Udało mi się spełnić wymagania konkursowe i mój projekt bierze udział w głosowaniu. Pomimo tego nie jestem zadowolony z tego co udało mi się osiągnąć. Plany miałem wielkie, a wyszło jak zawsze. Nie udało mi się wykonać nawet zadowalającej części z zaplanowanych funkcjonalności, nie pisałem tak często jak chciałem na początku. Pomimo tych niepowodzeń absolutnie nie żałuję decyzji o wzięciu udziału w konkursie. Nauczyłem się dzięki niemu o wiele więcej, niż byłbym w zrobić bez takiej motywacji. Na początek przemyśleń chciałbym pożalić się co było złe i godne pożałowania.

Porażki

Najważniejsza i najbardziej bolesna porażka – nie udało się wykonać większości z zaplanowanych funkcji. Jedyne co na chwilę obecną udostępnia Chupacabra to przeglądanie zdjęć znajdujących się na dysku. Nie została wykorzystana baza danych razem z mapowaniami NHibernate’a, nie ma możliwości grupowania zdjęć w albumy, nie można edytować i retuszować zdjęć, nie ma mowy o funkcjach społecznościowych. Wybujałe plany przegrały z szarą rzeczywistością. Z brakiem czasu i nierzadko energii, z brakiem motywacji i przekonania, że ta praca ma sens, z prostymi, często trywialnymi błędami, które zatrzymywały postępy na kilka godzin i odbierały ochotę do dalszej pracy.

Zawiodło również regularne aktualizowanie bloga. Co prawda, udało się w sam raz wpasować w wymaganą regulaminem ilość postów, jednak pozostaje niedosyt. Planowałem pisać często i ciekawie. Zamiast tego posty wychodziły rzadko, a do tego niektóre były bardzo długie, co z pewnością odstraszyło wielu potencjalnych czytelników. Rozczarowałem się webowym edytorem Bloggera. Pisanie postów nie było intuicyjne i kilka razy formatowanie rozjechało się po wstawieniu kodów źródłowych. Na szczęście tego posta (i mam nadzieję, że także następne) piszę z Windows Live Writer, który spełnia zadanie znakomicie. Być może coś jeszcze o nim napiszę.

Nie do końca jestem także zadowolony ze swojego wkładu w pozostałe projekty. Początkowo planowałem na bieżąco śledzić poczynania innych uczestników, komentować ich posty i być może postarać się czasem pomóc. Niestety tu też nie wyszło idealnie, przez długi czas udawało mi się w miarę uważnie przeczytać większość nowych postów, ale pod koniec wiele artykułów pominąłem, nie wspominając już o aktywnym komentowaniu. Jestem pod wielkim podziwem niektórych projektów i dziękuję ich autorom za inspiracje i ciekawe materiały.

Sukcesy

Największym sukcesem zdecydowanie jest wiedza, którą zdobyłem dzięki uczestnictwu w konkursie. Wiedza, której najprawdopodobniej nie zdobyłbym, gdyby tego konkursu nie było. Krótkie podsumowanie czego udało mi się dowiedzieć w trakcie tych tygodni konkursu:

  • Mercurial – użycie go do zarządzania kodem projektu sprawiło, że zakochałem się w rozproszonych systemach kontroli wersji i teraz na każdym kroku go wykorzystuję, do notatek, sprawozdań i wszystkich innych kodów.
  • NHibernate – idea wszelakich ORMów również przypadła mi bardzo do gustu i mam zamiar pogłębić ten temat. Co prawda moja znajomość tego frameworka jest na razie mocno podstawowa, jednak najważniejsze już wiem i przejście do bardziej zaawansowanych spraw nie powinno być problemem
  • WPF i MVVM – to moja porażka wśród sukcesów. Poznałem podstawy WPF’a i wykorzystywałem wzorzec MVVM, jednak jeszcze nie czuję do końca wszystkich mechanizmów i zachowań. Potrzebuję przeczytać książkę o WPFie od podstaw i wszystko sobie usystematyzować.
  • Coderush Express – znakomite narzędzie, które z pewnością będę nadal stosował, aż do momentu kiedy będzie mnie stać na wykupienie pełnej wersji :)
  • NUnit – już od dawna zbierałem się do wykorzystania testów jednostkowych w projekcie i wreszcie nadszedł ten czas.

Dzięki konkursowi wreszcie odważyłem się założyć własnego bloga. To bardzo duży sukces. Szczególnie, gdy spojrzałem na statystyki (nie przyszłoby mi na myśl je sprawdzić, gdybym nie zauważył tego w podsumowaniach innych uczestników :D). Jak dla mnie były one wręcz niebywałe, nie spodziewałem się, że tyle osób odwiedzało tego bloga (co prawda nie znam statystyk jak długo pozostawali na stronie i ile osób faktycznie przeczytało artykuły). Znakomitą większość wejść zawdzięczam serwisowi dotnetomaniak, kilka z moich postów się tam znalazło i wygenerowały całkiem spory ruch. W sumie ponad 4200 wejść i kilka postów z ponad 300 odwiedzinami. I jako ciekawostka: po wpisaniu w Google hasła mvvm, mój post pojawia się na piątym miejscu :P. Dla mnie jest bardzo miłe zaskoczenie.

Co dalej?

Blog mam nadzieję nie umrze tuż po zakończeniu konkursu. Co prawda nie planuję już pisać o projekcie konkursowym, ale chciałbym dodawać posty o tym o czym ten blog ma być. O programowaniu w dotnecie. Chciałbym uczyć się nowych zagadnień. Może ASP.NET? Albo Silverlight? A może na początek ugruntowanie wiedzy z WPF’a? Coś się na pewno znajdzie. Dlatego też cieszyłbym się, gdyby wszyscy nie usunęli RSS’a z mojego bloga tuż po zakończeniu konkursu. Może uda mi się coś wartościowego napisać.

Podziękowania

Czas na podziękowania. Przede wszystkim należą się one Maćkowi Aniserowiczowi za zorganizowanie tego konkursu i za nakłonienie nie tylko mnie, ale i wielu innych uczestników, do pracy i samodoskonalenia. Ponadto jestem winny podziękowania wszystkim, którzy mnie przekonali, żebym wziął udział w konkursie, że jednak warto i nic przez to nie stracę. I oczywiście wszystkim, którzy tego bloga czytali i komentowali, podsyłając kilka ciekawych propozycji i sugestii.

Dziękuję i zapraszam do odwiedzania bloga w przyszłości.

niedziela, 7 listopada 2010

Przeglądanie zdjęć na dysku

Czas żeby wykorzystać napisaną w ostatnim poście kontrolkę do wyświetlania obrazków przedstawioną w poprzednim poście. Na razie wykorzystywana będzie jedynie podczas przeglądania struktury dysku. Sprzężenie między aktualnym katalogiem a zdjęciami również będzie tymczasowe.

Aby w prosty sposób móc przekazać listę zdjęć do kontrolki ImageListView wystawiona na zewnątrz niej zostanie jedna DependencyProperty typu ObservableCollecion<Photo>. Dzięki temu bindowanie może być zrobione z poziomu XAML'a. 
public ObservableCollection<Photo> Photos
{
 get
 {
  return (ObservableCollection<Photo>)GetValue(PhotosProperty);
 }
 set
 {
  SetValue(PhotosProperty, value);
 }
}

public static readonly DependencyProperty PhotosProperty = DependencyProperty.Register(
 PhotosPropertyName,
 typeof(ObservableCollection<Photo>),
 typeof(ImageListView),
 new PropertyMetadata(new ObservableCollection<Photo>(), new PropertyChangedCallback(OnPhotosListChanged)));
Przy każdej zmianie zawartości listy zdjęć zostanie wywołana funkcja OnPhotosListChanged. Jej jedynym zadaniem będzie wysłanie komunikatu poprzez niedawno stworzoną statyczną klasę AppMessages.
private static void OnPhotosListChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
 AppMessages.PhotoSourceChangedMessage.Send(e.NewValue as ObservableCollection<Photo>);
}
Wiadomość ta jest odbierana w konstruktorze kontrolki i po prostu przypisuje nowo otrzymaną listę to właściwości użytej do przechowywania listy:
AppMessages.PhotoSourceChangedMessage.Register(this,
 p => Photos = p);

Do przeglądania zdjęć na dysku potrzebne jest jedynie, aby lista zdjęć była odświeżona za każdym razem, gdy zmieni się zaznaczony folder. Można to osiągnąć przez zbindowanie aktualnie wybranego katalogu z kontrolki DirectoryTreeView do właściwości Photos kontrolki ImageListView. 
<my:ImageListView Photos="{Binding CurrentDirectory, Converter={StaticResource DirectoryToPhotosConverter}, ElementName=directoryTreeView}"/>
Oczywiście nie zrzucamy na kontrolkę wyświetlającą odpowiedzialności z pobranie listy zdjęć z dysku. Dlatego też użyty jest konwerter zamieniający ścieżkę do katalogu na listę obiektów Photo, wskazujących na zdjęcia z tego katalogu. 
[ValueConversion(typeof(string), typeof(ObservableCollection<Photo>))]
public class DirectoryToPhotosConverter : IValueConverter
{
 public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 {
  string str = value as string;

  if (str != null)
  {
   IList<Photo> photos = ImageLoader.GetListOfPhotosFromPath(str);
   if (photos != null)
    return new ObservableCollection<Photo>(photos);
  }

  return new ObservableCollection<Photo>();
 }

 public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 {
  throw new NotImplementedException();
 }
}
Konwerter ten wykorzystuje metodę GetListOfPhotosFromPath z klasy ImageLoader:
public class ImageLoader
{
 public static List<Photo> GetListOfPhotosFromPath(string path)
 {
  List<Photo> result = new List<Photo>();

  if (Directory.Exists(path))
  {
   result.AddRange(getPhotosByExt(path, "*.jpg"));
   result.AddRange(getPhotosByExt(path, "*.jpeg"));
   result.AddRange(getPhotosByExt(path, "*.png"));
   result.AddRange(getPhotosByExt(path, "*.gif"));
  }

  return result;
 }

 private static List<Photo> getPhotosByExt(string path, string ext)
 {
  List<Photo> result = new List<Photo>();

  string[] files = Directory.GetFiles(path, ext);
  foreach (var f in files)
  {
   result.Add(new Photo
   {
    Path = f,
    Description = Path.GetFileName(f)
   });
  }
  return result;
 }
}
Jest to trochę naiwna implementacja zakładająca, że istnieją tylko cztery rodzaje plików graficznych, ale to sprawdzenia poprawności działania w zupełności wystarcza.

Po połączeniu tych elementów można już przeglądać katalogi w poszukiwaniu zdjęć :P

piątek, 5 listopada 2010

Wyświetlanie listy zdjęć

Czas na kolejną kontrolkę, która jest jednym ze składników okna głównego. Mianowicie kontrolkę pozwalającą wyświetlać kolekcję zdjęć w najprostszej tabelarycznej formie. Może się ona przydać zarówno do wyświetlania zawartości albumów jak i do przeglądania struktury dysku i podglądu zdjęć z katalogów. 

Oprócz samego zdjęcia chciałbym wyświetlać także opis, jeśli taki jest przypisany do zdjęcia (jeśli nie ma to po prostu nazwę pliku). Dlatego też wygodnie będzie wykorzystać odpowiedni DataTemplate, który z listy obiektów typu Photo (dokładnie tego, który jest wykorzystywany jako jeden z modeli przy współpracy z NHibernate) utworzy szereg obrazów wraz z podpisem. Lista zdjęć przechowywana będzie w obiekcie ViewModel jako prosta właściwość implementująca IPropertyChanged:
private ObservableCollection<Photo> _photos = new ObservableCollection<Photo>();
public ObservableCollection<Photo> Photos
{
 get
 {
  return _photos;
 }

 set
 {
  if (_photos == value)
   return;

  _photos = value;

  // Update bindings, no broadcast
  RaisePropertyChanged(PhotosPropertyName);
 }
}
Na początek należy przygotować kontener, do którego zbindowane będą zdjęcia i wskazać mu szablon danych (który zostanie omówiony w następnej kolejności). 

 
  
   
    
   
  
 

Jako kontener wybrałem WrapPanel, ponieważ sam dobiera ilość wyświetlanych kolumn w zależności od dostępnego miejsca. 

Teraz pora na utworzenie szablonu itemsTemplate, który będzie definiował wygląd każdego elementu umieszczanego we WrapPanel'u. Pojedynczy element będzie składał się z obiektu Image, dla którego właściwość Source ustawiona będzie na ścieżkę dostępu do pliku ze zdjęciem oraz obiektu TextBlock wyświetlającego opis obrazka. Wewnątrz DataTemplate'u może być umieszczony tylko jeden element, dlatego też będą one opakowane w StackPanel. W uproszczeniu prezentuje się to następująco:

 
  
  
 

Dodatkowo chciałem, aby kliknięcie na zdjęcie albo podpis (ogólnie na cały StackPanel) powodowało podjęcie odpowiedniej akcji. Niestety okazało się to trudniejsze niż myślałem. Wszystkie elementy będące wewnątrz WrapPanel'u przejęły DataContext od właściwości ItemsSource z kontenera ItemsControl, czyli nie miały pojęcia o istnieniu ImageListViewModel i nie tym samym nie było możliwości wywołania odpowiedniej komendy. Rozwiązaniem okazało się zastosowanie bardzo rozbudowanego bindingu do obiektu EventToCommand pochodzącego z MVVM Light Toolkit:

 
  
   
   
  
 
 ...

Taki binding przeszukuje w górę hierarchię obiektów, aż do momentu znalezienia obiektu typu ItemsControl, wtedy wykorzystuje jego DataContext w celu wywołania komendy. Jako parametr przekazywany jest cały obiekt typu Photo przypisany do aktualnego elementu. Nie jest ona specjalnie skomplikowana, ma za zadanie jedynie przesłanie odpowiedniego komunikatu:
private RelayCommand<Photo> _onClickCommand = null;
public RelayCommand<Photo> OnClick
{
 get {
  if (_onClickCommand == null)
  {
   _onClickCommand = new RelayCommand<Photo>(
    p =>
    {
     AppMessages.PhotoSelectedChanged.Send(p);
    });
  }
  return _onClickCommand;
 }
}
Jest to bardzo podstawowy zakres funkcjonalności jakie spadają na tę kontrolkę. Nie mam do końca zdefiniowanej wizji co do ostatecznego połączenia kontrolek i ich funkcjonalności (na przykład na której powinien ciążyć obowiązek wyświetlania tagów i daty dodania). W zależności od tego co się wyklaruje być może ta kontrolka zostanie jeszcze rozszerzona.

piątek, 29 października 2010

Prosty wrapper Messengera z MVVM Light Toolkit

Od razu zaznaczam, że pomysł nie jest mój, ale jest zaczerpnięty z przykładowego projektu na CodeProject. Jednak spodobał mi się bardzo, bo wprowadza porządek do projektu, jest prosty i oczywisty. 

W czym problem?
Podczas korzystania z Messengera, problemem okazało się tworzenie unikalnych tokenów za pomocą, których można by jednoznacznie identyfikować komunikaty. Dzięki tokenom można przesyłać wiele komunikatów tego samego typu (na przykład w prostym przypadku string'ów) i jednocześnie móc je od siebie odróżnić. Pozwala to także uniknąć konieczności tworzenia własnych klas komunikatów nawet dla prostych przypadków. 
Używanie ciągów znakowych wpisywanych z palca jako tokenów (na przykład "SelectedDirectoryChanged") jest niewygodne, podatne na literówki i nie daje żadnego wsparcia dla IntelliSense'a. Chciałem rozwiązać ten problem przez przechowywanie takich tekstowych tokenów w zasobach, ale okazało się to niewygodne i kłopotliwe. 

Rozwiązanie
Aby poradzić sobie z tym problem i jeszcze dodatkowo za darmo otrzymać wygodniejszy sposób posługiwania się komunikatami należy stworzyć statyczną klasę (AppMessages), która będzie wrapperem na Messengera. Najpierw deklarujemy enum'a, który będzie przechowywał wszystkie typy komunikatów jakie będziemy przesyłać:
enum MessageTypes
{
 SelectedDirectoryChanged,
 PhotoSourceChanged,
 AddPhoto, 
 SavePhoto
}
Następnie dla każdego komunikatu należy stworzyć osobną statyczną klasę wewnątrz AppMessages, która będzie posiadać dwie funkcje Send oraz Register. Ich jedynym zadaniem jest wywołanie odpowiednich funkcji Messengera, np:
public static class SelectedDirectoryMessage
{
 public static void Send(Models.DirectoryItem item)
 {
  Messenger.Default.Send<models.directoryitem>(item, MessageTypes.SelectedDirectoryChanged);
 }

 public static void Register(object recipient, Action<models.directoryitem> action)
 {
  Messenger.Default.Register<models.directoryitem>(recipient, MessageTypes.SelectedDirectoryChanged, action);
 }
}
Dzięki takiemu podejściu zyskujemy bardzo prosty i czytelny sposób obsługi komunikatów:
//rejestrowanie akcji reagującej na komunikat
AppMessages.SelectedDirectoryMessage.Register(this,
 item =>
 {
  this.CurrentDirectory = item.Path;
 });
 
//wysyłanie komunikatu
AppMessages.SelectedDirectoryMessage.Send(_fileitem);
Unikamy wpisywania za każdym razem tasiemców, w których trzeba podawać typy przesyłanych danych, robimy to tylko raz pisząc klasę obsługi komunikatu. 

Snippet do generowania klas obsługi komunikatów
Tworzenie nowych klas dla nowych komunikatów jest wtórne i powtarzalne, dlatego zrobiłem sobie snippet który generuje nową klasę i pozwala wpisać wszystkie parametry. Nie ma w nim nic nadzwyczajnego, ale pomimo tego jakby ktoś chciał go użyć wrzuciłem go tutaj.

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.