czwartek, 31 stycznia 2013

[WPF] Caliburn.Micro: Window Manager

Window Manager, jak nazwa wskazuje, służy do zarządzania trzema rodzajami okienek. Serwis ten dodawany jest do kontenera IoC w bootstrapperze przy zainstalowaniu Caliburna w naszym projekcie. Dostępne rodzaje okienek to:
  • Window - niezależne osobne okno z paskiem do zamykania, minimalizowania itd.
  • Popup - nie blokuje okna - rodzica, brak paska do zamykania
  • Dialog - blokuje okno - rodzic, dostępny pasek do zamykania
Każde osobne okienko powinno być osobnym widokiem z własnym ViewModelem. W ViewModelu można obsłużyć zdarzenie zamknięcia okienka.

public class PopupPersonViewModel : Screen
{
    public Person Entity { get; set; }

    public PopupPersonViewModel(Person person)
    {
        Entity = person;
    }

    public override void TryClose(bool? dialogResult)
    {
        IoC.Get<IEventAggregator>().Publish(new PersonUpdatedMessage(){Entity = Entity});
        base.TryClose(dialogResult);
    }
}

Samo otwarcie okna jest bardzo proste:

public void OpenWindow()
{
    dynamic settings = new ExpandoObject();
    settings.WindowStartupLocation = WindowStartupLocation.Manual;
    _windowManager.ShowWindow(new PopupPersonViewModel(Entity), null, settings);
}

środa, 30 stycznia 2013

[WPF] Caliburn.Micro: Akcje

W tym poście nieco na temat sposobów, jak powiązać akcje użytkownika z widoku z handlerami z ViewModelu.

Najprostszy sposób to binding przez konwencje nazw, ale sposób ten nie zawsze wystarcza. Caliburn.Micro, dla niektórych wybranych kontrolek, wiąże atrybut x:Name (z Xaml) z funkcją o identycznej nazwie z ViewModelu. W ten sposób nie jesteśmy jednak w stanie sprecyzować, które zdarzenie z kontrolki nas interesuje. Twórcy uznali, że np. dla przycisku będzie to zdarzenie Click, które zdecydowanie najczęściej ma nieść za sobą określone akcje użytkownika. Zainteresowani, mogą przejrzeć domyślne bindingi w klasie ConventionManager.cs ze źródeł projektu Caliburn.Micro.

Inny bardziej złożony, ale dający więcej możliwości sposób wykorzystuje Triggery.

<Button Content="Print Name">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <cal:ActionMessage MethodName="PrintName" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

Dzięki konwencjom możemy sterować dostępnością przycisku. Wystarczy wprowadzić property o nazwie takiej samej jak akcja, lecz zawierającej z przodu człon Can. Property takie musi zwracać typ bool i być notyfikowane przy odpowiednich zmianach w modelu danych. Na przykład:

public class PersonDetailsViewModel : Screen, IHandle<NewPersonMessage>
{
    public bool CanPrintName
    {
        get { return Entity != null; }
    }

    public void Handle(NewPersonMessage message)
    {
        Entity = message.Entity;
        NotifyOfPropertyChange(() => CanPrintName);
    }

    public void PrintName()
    {
        MessageBox.Show(Entity.Name);
    }
}

Ostatnim sposobem zapisu, krótszym i umożliwiającym przekazywanie parametrów w prostszy sposób jest Message.Attach. Tutaj w pełnym zapisie możemy wybrać, które zdarzenie nas interesuje, oraz co chcemy przekazać przez parametr.

<Slider x:Name="Entity_Age" Maximum="100" VerticalAlignment="Stretch"
    cal:Message.Attach="[Event MouseEnter] = [Action IncrementAge($eventArgs)]" />

Poza eventArgs można przekazać kilka innych obiektów, między innymi związanych z data bindingiem. Opisane są w dokumentacji.

wtorek, 29 stycznia 2013

[WPF] Caliburn.Micro: EventAggregator

Do komunikacji pomiędzy ViewModelami, Caliburn Micro wykorzystuje EventAggregatora. Tworzenie pojedynczej instancji ma miejsce w klasie AppBootstrapper (dodawanej do naszego projektu podczas instalacji Caliburna w projekcie), a konkretnie w funkcji Configure.

protected override void Configure() {
    var catalog = new AggregateCatalog(
        AssemblySource.Instance.Select(x => new AssemblyCatalog(x)).OfType<ComposablePartCatalog>()
        );

    container = new CompositionContainer(catalog);

    var batch = new CompositionBatch();

    batch.AddExportedValue<IWindowManager>(new WindowManager());
    batch.AddExportedValue<IEventAggregator>(new EventAggregator());
    batch.AddExportedValue(container);
    batch.AddExportedValue(catalog);

    container.Compose(batch);
}

W ViewModelach EventAggregator dostępny jest dzięki kontenerowi IoC. Dobrą praktyką jest, aby wiadomości nadawane przez EventAggregatora były osobnych, dedykowanych w tym celu typów. Aby nadać taką wiadomość wystarczy w danym ViewModelu:

var person = ea.AddedItems[0] as Person;
var msg = new NewPersonMessage() {Entity = person};
var evag = IoC.Get<IEventAggregator>();
evag.Publish(msg);

Po stronie subskrybenta należy implementować interfejs IHandle<T> z konkretnym typem wiadomości obsługiwanym w funkcji Handle(T).

public class PersonDetailsViewModel : Screen, IHandle<NewPersonMessage>
{
    public PersonDetailsViewModel()
    {
        var ea = IoC.Get<IEventAggregator>();
        ea.Subscribe(this);
    }

    public void Handle(NewPersonMessage message)
    {
        Entity = message.Entity;
    }
}

niedziela, 27 stycznia 2013

[WPF] Caliburn.Micro: Kolekcje

Podczas bindowania kolekcji najpierw należy zdefiniować model danych. Przykładowa klasa reprezentująca dane osoby.

public class Person
{
    public int Age { get; set; }
    public string Name { get; set; }
    public bool IsMale { get; set; }
}

Następnie w ViewModelu definiujemy obserwowalną kolekcję, która zapewni nam synchronizację widoku z modelem danych.

public class PersonListViewModel : Screen
{
    public ObservableCollection<Person> Items { get; set; }

    private Person _selectedItem;
    public Person SelectedItem
    {
        get { return _selectedItem; }
        set { _selectedItem = value; }
    }

    public PersonListViewModel()
    {
        Items = new ObservableCollection<Person>
        {
            new Person{ Age = 15, IsMale = true, Name= "Michael"},
            new Person{ Age = 80, IsMale = true, Name= "Steven"},
            new Person{ Age = 25, IsMale = false, Name= "Kate"},
            new Person{ Age = 64, IsMale = true, Name= "Matthew"}

        };
    }
}

Dodatkowo, dzięki konwencjom, property SelectedItem będzie synchronizowane z wybranym elementem z kolekcji Items.
Ostatni krok to binding w ViewModelu. Kolekcje można wyświetlać w kilku elementach WPFa. Poniżej pokazano, jak to zrobić w ListBoxie.

<ListBox x:Name="Items" 
         cal:Message.Attach="[Event SelectionChanged] = [Action NewSelectedItem($eventargs)]"
         SelectionMode="Single">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Width="200" Height="100" cal:Bind.Model="{Binding}" >
                <TextBlock x:Name="Name" VerticalAlignment="Top" />
                <Label x:Name="Age" VerticalAlignment="Top" />
                <CheckBox x:Name="IsMale" VerticalAlignment="Top" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Podobnie jak w przypadku skalarnych propercji, zaczynamy od ustawienia atrybutu x:Name. Dzięki cal:Message.Attach możemy przypisywać do zdarzeń widoku funkcje z ViewModelu. Następnie dla całego ListBoxa definiuje się ItemTemplate, w którym na kontenerze zaznaczamy binding do propercji poprzez atrybut cal:Bind.Model. Poziom niżej w drzewie wizualnym możemy już bindować się do pojedynczych propercji poprzez standardowe x:Name.

sobota, 26 stycznia 2013

[WPF] Caliburn.Micro: Wprowadzenie

Caliburn.Micro to prosty a zarazem dający wiele możliwości framework dla aplikacji tworzonych w oparciu o XAML i wzorzec MVVM. Aby skorzystać z Caliburna należy doinstalować go do naszego projektu przy pomocy managera pakietów NuGet z VisualStudio.



Jedną z najlepszych funkcjonalności jest data binding rozwiązany poprzez konwencje. Aby powiązać ze sobą View oraz ViewModel, programista nie musi dopisywać żadnych specjalnych instrukcji. Wszystko zapewnia nam zachowanie odpowiedniej konwencji nazw dla naszych klas. Przykładowo, domyślny widok startowy po zainstalowaniu Caliburna to ShellView. Aby bindować do niego dane jako propercje ViewModelu wystarczy nazwać go zamieniając View na ViewModel.

<Window x:Class="CaliburnMicroSampleWPF.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cal="http://www.caliburnproject.org"
        Width="400" Height="400"
        >
 <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        
        <ContentControl x:Name="Item" Grid.Column="0">            
        </ContentControl>
        <ContentControl x:Name="Details" Grid.Column="1">            
        </ContentControl>
    </Grid>
</Window>


[Export(typeof(IShell))]
    public class ShellViewModel :  Screen, IShell 
    {
        private object _item;
        public object Item 
        {
            get { return _item; }
            set
            {
                _item = value;
                NotifyOfPropertyChange("Item");
            }
        }
        public object Details { get; set; }

        public ShellViewModel()
        {
            Item = new PersonListViewModel();
            Details = new PersonDetailsViewModel();
        }
    }

Elementowi z XAML, który ma ustawiony atrybut x:Name, odpowiada property z ViewModelu o takiej samej nazwie. Tak działa data binding w Caliburnie. Powyższy kod pokazuje dodatkowo, jak wstawiać podwidoki do widoku okna. Wystarczy po stronie widoku skorzystać z ContentControl, a po stronie ViewModelu stworzyć properties typu object. To wystarczy do zbindowania danych, natomiast wstawieniem konkretnego podwidoku można sterować poprzez przypisywanie do propercji odpowiednich ViewModeli.

O tym, który widok będzie wyświetlany przy starcie aplikacji decyduje klasa AppBootstrapper.cs, a konretnie jej nagłówek:

public class AppBootstrapper : Bootstrapper<IShell>

W tym przypadku uruchamiany będzie widok, którego ViewModel dziedziczy z interfejsu IShell.

piątek, 25 stycznia 2013

Bazy danych

Wszystko na temat baz danych: zarówno relacyjnych, wraz z narzędziami ORM, jak i nowego trendu NoSQL. Spis postów na temat ORM i SQL

[SQL|ORM] Entity Framework : Code First

Code First to rozwiązanie zaproponowane w EF 4.1, wydanym w kwietniu 2011. Nie jest zatem częścią .NETa 4.0 i VS 2010. Aby skorzystać z tej wersji EF w Visual Studio należy doinstalować najnowszą wersję EF (obecnie 5.0), najlepiej przy użyciu menadżera pakietów NuGet.

W podejściu CodeFirst, modelem danych stają się klasy użytkownika, które można przetransformować na tabele relacyjnej bazy danych. Główna różnica polega na tym, że nie mamy już do czynienia z plikami .edmx. Metadane dla klas tworzone są w pamięci, a nie w plikach xmlowych. Tabele, które zostaną stworzone możemy konfigurować przy użyciu atrybutów nad propercjami lub całymi klasami. Drugie podejście to tzw. Fluent API, czyli konfiguracja poprzez wywołania specjalnych funkcji.

Poniżej prosty przykład jak stworzyć tabelę w CodeFirst. Domyślnie, jeżeli nie skonfigurujemy connectionString, utworzy nam się baza danych w SQL Express o nazwie złożonej z nazwy namespace i nazwy klasy kontekstu.

public class Blog
    {
        public Blog()
        {
            Posts = new List<Post>();
        }

        public int Id { get; set; }
        public string Title { get; set; }
        public string BloggerName { get; set; }
        public List<Post> Posts { get; set; }
        public string BlogCode
        {
            get { return Title.Substring(0, 1) + ":" + BloggerName.Substring(0, 1); }
        }
    }

public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        public virtual List<Blog> Blogs { get; set; }
    }

public class Context : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    }

var db = new Context();
var blog = new Blog() {BloggerName = "Julie", Title = "EFBlog"};
db.Blogs.Add(blog);
db.SaveChanges();

Klasę kontekstu tworzymy dziedzicząc po typie DbContext wprowadzonym również w EF4.1. Klucze główne dla encji ustawiane są przez konwencję, tak więc np. dla klasy Blog, kluczem głównym będzie property o nazwie Id lub BlogId. Dla tabeli Post utworzony zostanie również domyślnie klucz obcy do tabeli Blog. Ponadto warto skonfigurować program tak, aby przy każdej zmianie modelu baza tworzyła się na nowo. Przy starcie aplikacji dodać należy następujące wyrażenie.

Database.SetInitializer(new DropCreateDatabaseIfModelChanges<Context>());

Gdzie Context to nazwa klasy dziedziczącej po DbContext.

Data Annotations

Data Annotations daje możliwość konfiguracji klas domenowych. W tym podejściu konfiguracja odbywa się poprzez atrybuty. Poniżej lista ciekawszych atrybutów
  • [Key] -  klucz główny na kolumnie nie obejmowanej konwencją
  • [MaxLength(int)] - ustawienie dotyczące ilości znaków dla danej kolumny
  • [Required] - mapowane na NOT NULL w SQL
  • [Column(String)] - mapowanie na kolumnę o danej nazwie
  • [Table(String)] - klasa mapowana na tabelę o podanej nazwie
  • [DatabaseGenerated(DatabaseGeneratedOption)] - można tutaj ustawić np Identity dla klucza głównego
  • [NotMapped] - nie transformowane do bazy danych
  • [ComplexType] - typy złożone jako propercje, poszczególne properties takiego typu zostaną przeklejone do tabeli odpowiadającej encji, w której taki typ się znajduje.
  • [ForeignKey(String)] - nazwa klucza obcego umieszczana nad referencją
public class Blog
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    [Required]
    public string Title { get; set; }
    [MaxLength(30)]
    public string BloggerName { get; set; }
    public List<Post> Posts { get; set; }
    [NotMapped]
    public string BlogCode
    {
        get { return Title.Substring(0, 1) + ":" + BloggerName.Substring(0, 1); }
    }
}

Fluent API

Wszystkie opcję dostępne z poziomu DataAnnotations  można konfigurować także przy użyciu tzw. FluentAPI, czyli po prostu korzystając z odpowiednich klas i metod.Dzięki takiemu podejściu nasze "Domain Objects" pozostaną niezależne od konfiguracji bazy danych.

public class Context : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        var blog = modelBuilder.Entity<Blog>();
        blog.Property(b => b.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
        blog.Property(b => b.Title).IsRequired();
        blog.Property(b => b.BloggerName).HasMaxLength(30);
        blog.Ignore(b => b.BlogCode);
        base.OnModelCreating(modelBuilder);
    }
}

FluentAPI udostępnia także kilka dodatkowych opcji niedostępnych z poziomu DataAnnotations, na przykład możliwość dzielenia encji na wiele tabel lub łączenia wielu encji w jedną tabelę.

niedziela, 13 stycznia 2013

[SQL|ORM] Entity Framework : Obiekty Entity Framework

Obiekty EF można dostosować do potrzeb aplikacji na bardzo wiele sposobów. W tym poście kilka użytecznych przykładów w tym temacie.

Wykonywanie własnego kodu, podczas wywołania metody SaveChanges

Wystarczy nadpisać metodę SaveChanges z obiektu kontekstu. Stan obiektów dostępny jest w ObjectStateManager.

public override int SaveChanges(SaveOptions options)
        {
            Console.WriteLine("Saving Changes");
            var applicants = this.ObjectStateManager
                                .GetObjectStateEntries(EntityState.Deleted)
                                .Select(e => e.Entity)
                                .OfType<Applicant>().ToList();
            int changes = base.SaveChanges(options);
            Console.WriteLine("\n{0} applicants deleted",
                applicants.Count().ToString());

            foreach (var applicant in applicants)
            {
                File.Delete(applicant.ResumePath);
                Console.WriteLine("\n{0}File deleted at {1}", applicant.Name,
                    applicant.ResumePath);
            }
            return changes;
        }

Reguły walidacyjne dla propercji 

Aby dodać prostą regułę walidacyjną należy na obiekcie encji nadpisać metody zgodne z nazwą property. Metody te generowane są przez EF. Na przykład dla property UserName mamy możliwe OnUserNameChanging oraz OnUserNameChanged. W tych metodach mamy dostęp do encji, więc możemy modyfikować jej pola. Metody takie wołane są nie tylko przy zmianie wartości property ale także przy materializowaniu encji podczas jej pobierania z bazy danych.

public partial class User
{
    partial void OnUserNameChanging(string value)
    {
        if (value.Length > 5)
            Console.WriteLine("{0}'s UserName changing to {1}, OK!",
                this.FullName, value);
        else
            Console.WriteLine("{0}'s UserName changing to {1}, TooShort!",
                this.FullName, value);
    }

    partial void OnUserNameChanged()
    {
        this.IsActive = this.UserName.Length > 5;
    }
}

Logowanie połączeń do bazy danych

Aby logować połączenia możemy zasubskrybować się na zdarzenie StateChange z property Connection dostępnej w obiekcie kontekstu.

this.Connection.StateChange += (s, e) =>
    {
        var conn = ((EntityConnection) s).StoreConnection;
        Console.WriteLine("{0}: Database: {1}, State: {2} was {3}",
                            DateTime.Now.ToShortTimeString(), conn.Database,
                            e.CurrentState, e.OriginalState);
    };

Walidacja przy SaveChanges

Czasami do walidacji potrzebujemy dwie wartości encji: nową i poprzednią (np. procentowy przyrost płac). Odpowiednim miejscem, w którym mamy dostęp do obu wartości jest zdarzenie SavingChanges.

public partial class EntityFrameworkRecipesEntities3
{
    partial void OnContextCreated()
    {
     this.SavingChanges += new EventHandler(EntityFrameworkRecipesEntities3_SavingChanges);
    }

    void EntityFrameworkRecipesEntities3_SavingChanges(object sender, EventArgs e)
    {
     var entries = this.ObjectStateManager
         .GetObjectStateEntries(EntityState.Modified)
         .Where(entry => entry.Entity is Employee);
     foreach(var entry in entries)
     {
      var salaryProp = entry.GetModifiedProperties()
           .FirstOrDefault(p => p == "Salary");
      if(salaryProp != null)
      {
       var originalSalary = Convert.ToDecimal(
            entry.OriginalValues[salaryProp]);
       var currentSalary = Convert.ToDecimal(
            entry.CurrentValues[salaryProp]);
       if(originalSalary != currentSalary)
       {
        if(currentSalary > 1.1M*originalSalary)
        {
         throw new ApplicationException("Can't increase salary more than 10%");
        }
       }
      }

     }
    }
}