Blog Kokosa

.NET i okolice, wydajność, architektura i wszystko inne

NAVIGATION - SEARCH

MemoryVisualizer - jak połączyć WPF z F#

Wstępniak: Uczę się używać F# wraz z postami o tym projekcie. Nie wszystko więc pewnie od razu będzie idealne. Za to w zamian cenne może być to, że dzielę się uwagami, spostrzeżeniami i zaskoczeniami w ramach tego procesu.

Po trzech pierwszych, wprowadzających postach pora wreszcie przejść do konkretów - kodu! MemoryVisualizer ma być aplikacją desktopową, napisaną w WPF. Nie mam jeszcze ani jednej linijki kodu, zacznę zatem od pustej aplikacji z jakiegoś szablonu. Gdybym zdecydował się na C#, sprawa byłaby prosta - startuję z pustego szablonu WPF Desktop Application i już. Mamy tam z pudełka obsługę XAML, code behind, designer itd. Jednak jak najwięcej chcę napisać w F#, zatem rodzi się pytanie - czy i jak zintegrować WPF z F#? Dawno, dawno temu pamiętam jeden sposób - normalna aplikacja C# + WPF i używanie bibliotek w F#. Ale to mało eleganckie, ja chcę pisać wszystko w F#! Unikam hybrydowego podejścia - widoki w C#, View Model i Model w F#. Jednak takie hybrydowe podejście okazuje się wciąż popularne, więc dla potomnych - jeśli Wam odpowiada, możecie pójść tą drogą.

Co więcej, skoro piszę funkcyjnie, zaczynam mieć wątpliwości czy na pewno podejście MVVM jest uzasadnione. Moja aplikacja jest prosta pod kątem GUI więc tak naprawdę, zapewne każde podejście się sprawdzi. Ale skoro już piszę, chcę poznać najlepsze wzorce. Nawet jeśli ich nie użyję, to przynajmniej świadomie.

Prace zacząłem od szeroko zakrojonych poszukiwań pod hasłem "WPF + F#". Zsyntetyzowane wyniki przedstawiają się następująco. Chcąc pożenić te dwie technologie mamy następujący wybór:

1. Goły F#

Nic nie stoi na przeszkodzie, by z gołej aplikacji konsolowej F# zrobić aplikację desktopową. Tworzymy F# Console Application, zmieniamy w ustawieniach projektu Output type na Windows Application. Trzeba potem dodać m.in. referencje do PresentationCore, PresentationFramework, System.Xaml and WindowsBase i wykonać coś w stylu:

let main argv = 
    let mainWindow = Application.LoadComponent(new System.Uri("/<Your Assembly Name>;component/MainWindow.xaml", UriKind.Relative)) :?> Window
    let application = new Application()
    application.Run(mainWindow)
Plik MainWindow.xaml możemy sami sobie stworzyć jako np. plik XML i zmienić mu rozszerzenie:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="WPF F#" Height="350" Width="525">
    <Grid Name="MainGrid">
    </Grid>
</Window>

Zmienić Build Action na Resource i opcję Copy to Output Directory ustawić na Do not copy. To zadziała bez problemu, zobaczymy swoje pierwsze F#-owe okienko.

Problem z tym podejściem jest taki, że nie dostajemy statycznie typowanego bytu opisującego nasz interfejs. W WPFie w wydaniu C# Visual Studio generuje partial class, który zawiera wszystkie kontrolki, właściwości itd. Tutaj musielibyśmy się do nich dostawać w dynamiczny sposób. Pytanie tylko, czy będzie mi tego brakować. Odnoszenie się do kontrolek jest potrzebne w code behind, a np. dobrze napisana aplikacja MVVM powinna sobie bez niego poradzić. Jeśli zatem jesteśmy dobrej myśli, takie podejście powinno wystarczyć. Wystarczy tylko lekko zmodyfikować aplikację, by ustawić jej DataContext:

let main argv = 
    let window = Application.LoadComponent(new System.Uri("/FSharpBareWPF;component/MainWindow.xaml", System.UriKind.Relative)) :?> Window
    window.DataContext <- new MainViewModel()
    (new Application()).Run(window)

I już możemy korzystać z niego w normalny sposób w XAMLu:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="{Binding Name}" Height="350" Width="525">
    <Grid Name="MainGrid">
    </Grid>
</Window>
Zakładając, że mamy prosty ViewModel:
type MainViewModel() = 
    let mutable name = "Hello from F# + WPF"
    
    member x.Name 
        with get () = name
        and set value = name <- value

Wyświetli nam się okienko z tytułem Hello from F# + WPF.     

2. F# Empty Windows App (WPF) template - użycie FsXaml

W galerii online szablonów dla Visual Studio znajdziemy jeden z dwóch, dedykowanych do F# i WPF. Ten z nazwą "Empty" w środku jest rzeczywiście absolutnie minimalną aplikacją okienkową. Właściwie realizuje dokładnie to samo, co omówiłem powyżej - tworzy puste okienko z określonym tytułem.

Główna różnica polega na tym, że w szablonie użyto dobrodziejstwa w postaci F#-owego type providera interpretującego XAML - o nazwie FsXaml. Type providery to temat na osobny wpis, a raczej całą książkę. Ale w dużym skrócie, interpretują one to co się im dostarczy, i dostarczają typy z odpowiednimi właściwościami, metodami, itp. Dostępne są type providery dla XML, JSON, CSV, SQL itp. itp. I jest też taki, który dostarczy nam obiekt reprezentujący XAML. Uzyskamy więc podobny efekt, jak generatory partial class dla WPF w C#.

W efekcie kod metody main trochę się uprości:

type MainWindow = XAML<"MainWindow.xaml", true>

let main argv = 
    let window = new MainWindow()
    window.Root.DataContext <- new MainViewModel()
    (new Application()).Run(window.Root)

Co więcej, zyskujemy wspomniany dostęp do nazwanych elementów:

fsharp5

Czy to się bardzo przyda, nie wiem. Ale skoro jest, warto mieć pod ręką. Bo coś czuję, na podstawie innych doświadczeń, że bez code behind się nie obędzie w przypadku kontrolki do rysowania grafów.

3. F# Windows App (WPF, MVVM)

Drugi z możliwych do pobrania szablonów online. Ten dla odmiany nie używa FsXaml, ale "gołego" podejścia, o którym pisałem wcześniej. Nadbudowuje to jednak przejrzystym przykładem użycia wzorca MVVM oraz bardziej rozbudowanym WPF - użyto m.in. styli i Data Template.

fsharp6

Mamy tu zatem malutki framework MVVM, na którego opis prawdopodobnie nie teraz miejsce i czas. Z punktu widzenia integracji WPF z F# nie dzieje się tu nic, o czym byśmy nie mówili wcześniej.

4. FSharp.ViewModule

Jak to ładnie określa strona projektu: "Library providing MVVM and INotifyPropertyChanged support for F# projects". Jest to zatem trochę bardziej rozbudowany mini-framework MVVM. Jeśli używaliście MVVM Light, to pewnie będzie coś podobnego.

Mam z tą biblioteką pewien problem, bo choć jest wspominana przez blogi i tutoriale, nie znalazłem konkretnej dokumentacji, co dokładnie potrafi. Tak jak np. w tym ciekawym artykule. Może to być trochę zniechęcające.

5. FSharp.Desktop.UI

Tu mamy ciekawostkę, która na dłużej zatrzymała mnie i zmusiła do myślenia. Na stronie projektu, jest on opisywany następująco:

"F# MVC for WPF is a small, yet powerful framework, designed for building WPF applications in F#. With strong support for MVC, functional, asynchronous and event-driven programming, it will enable you to build your solution quickly, without the need to sacrifice type system or testability."

Brzmi atrakcyjnie, ale szczególnie zwróciłem uwagę na fakt, że zostało tu użyte podejście MVC, a nie MVVM. Autorem biblioteki jest Dmitrij Morozov, ekspert F#, jeden z współautorów książki F# Deep Dives. Zresztą została ona tam przez niego opisana (w uproszczonej wersji) w rozdziale 7.

Czemu MVC? Jak twierdzi Dmitrij, wzorzec MVVM doskonale się wpasowuje w programowanie obiektowe. Skupia bowiem stan i zachowanie (właściwości i metody) w jednym monolitycznym ViewState. Kłóci się to z podejściem funkcyjnym, gdzie niemutowalne dane (stan) są rozgraniczone z zachowaniem (funkcjami). Ładnie się przedstawia jego tok myślenia doprowadzający do powstania owego frameworku MVC. Można go prześledzić w serii jego wpisów F# WPF MVC oraz we wspomnianym rozdziale książki. Brzmi to świetnie i bardzo funkcyjnie, np. widok jest w tym podejściu potraktowany jako strumień zdarzeń. Polecam świetny artykuł F# Xaml application - MVVM vs MVC, w którym autor implementuje tę samą, prostą funkcjonalność w obu podejściach. Oraz równie ciekawy tutorial Functional Reactive Programming in F# and WPF.

6. Werdykt

No dobrze, myślę, że w miarę kompletnie wyczerpałem listę możliwości jak ugryźć aplikację WPF w F#. Ta wiedza może się przydać każdemu.

Co do mojego projektu, pora na ostateczne decyzje i pierwsze commity. Serce wyrywa się do podejścia z użyciem FSharp.Desktop.UI. Dużo bardzo ciekawych rzeczy się tam dzieje i zrozumienie oraz zastosowanie tego podejścia dałoby mi zapewne "plus milion" do wiedzy. Funkcjonalne programowanie reaktywne interfejsu użytkownika, mniam! Ale obawiam się, że zbyt zablokowałoby to moje prace. Poza tym moja aplikacja jest bardzo prosta. Ma jeden wielki model (memory dump), maksymalnie dwa lub trzy widoki. Coś takiego jak zdarzenia praktycznie nie istnieje - głównym zdarzeniem będzie wykonanie zapytania i wyświetlenie grafu z wynikiem. Zatem byłoby to wszystko strzelaniem z armat do muchy. Podejście to na pewno świetnie sprawdza się w aplikacji gdzie "dużo się dzieje". Okienkowa wersja Twittera, Facebooka, notowań giełdowych itd., gdzie zdarzenia fruwają na potęgę, to jego żywioł.

Cukierki zatem odstawiamy na bok. Znacznie lepiej dla mnie będzie (zgodnie z podejściem MVP opisanym poprzednio) użyć czegoś lekkiego. Dlatego poprzestanę na razie po prostu na gołym podejściu z pomocą FsXaml. Jeśli zacznie mi brakować jakiś elementów MVVM, to dodatkowo użyję FSharp.ViewModule. Ale to już wyjdzie w praniu.


Dodatkowa lektura:

blog comments powered by Disqus