środa, 18 stycznia 2012

WPF: o zasobach (resources) coś więcej [PL]

W poprzednim wpisie pisałem o tym, że zamiast wyznaczać ścieżki do obrazków, lepiej umieścić je w zasobach. Temat wydawał mi się prosty, ale w komentarzach pojawiła się odrobinę odmienna opinia:
"Nie, obrazków nie trzyma się w zasobach. Ikony, czasem, jak potrzeba, jeden, dwa obrazki, ale nie więcej. One są za duże - EXE-k puchnie, robi się bałagan, trzeba rekompilować, by zmienić obrazek, a i krótsze nie jest. Tą linijkę równie dobrze można zapisać jako:
ImageBrush brush = new ImageBrush(new BitmapImage(new Uri("Images/test.png", UriKind.Relative)));
Można to wpakować w jakąś statyczną klasę helpera, by nie pisać nie wiadomo ile razy."
W tym wpisie postaram się wyjaśnić i uzupełnić informacje związane z zasobami w WPF. Moim celem nie jest kompletna negacja przedstawionej opinii, ale przyjrzyjmy się, co daje nam .NET Framework i jak to można wykorzystać, by nasza aplikacja była lepsza.
Zacznijmy od prostego stwierdzenia: są dwa typy zasobów:
  • Zasoby w sensie XAML
  • Zasoby w sensie plików osadzonych w jakimś assembly

Zasoby (resources) w XAML

Zauważmy, że WPF pozwala nam definiować zasoby w XAML, można to zrobić bezpośrednio w kodzie XAML okna lub kontrolki (w poprzednim wpisie właśnie był taki przykład) lub lepiej zasób zdefiniować w pliku App.xaml (wspólnym dla całej aplikacji) lub najlepiej w ResourceDictionary.
Zobaczmy poniższy przykład, w którym zasób zadeklarowano w pliku App.xaml:
<Application x:Class="WpfApplication_resources_tests.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <BitmapImage x:Key="MyAppImageSource" UriSource="./ImagesToBeCopied/logo_zielone_tlo.jpg"  />
        <ImageBrush  x:Key="MyAppImageBrush" ImageSource="{StaticResource ResourceKey=MyAppImageSource}"/>
    </Application.Resources>
</Application>
Zauważmy, że obrazek nie został dodany do assembly, a tylko skopiowany do katalogu aplikacji:
I wykorzystano go poprzez:
<Window x:Class="WpfApplication_resources_tests.Window2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window2" Height="300" Width="300">
    <Grid>
        <Label Background="{StaticResource ResourceKey=MyAppImageBrush}" Width="50" Height="50" 
               VerticalAlignment="Top" HorizontalAlignment="Left"/>
        <Viewport3D Name="mainViewport">
            <Viewport3D.Camera>
                <PerspectiveCamera FarPlaneDistance="100" LookDirection="-1,-1,-1"
              UpDirection="0,1,0" NearPlaneDistance="1" Position="3,3,3" FieldOfView="70" />
            </Viewport3D.Camera>
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <DirectionalLight Color="White" Direction="-1,-1,-1" />
                </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <GeometryModel3D>
                        <GeometryModel3D.Geometry>
                            <MeshGeometry3D Positions="-2 -2 0  2 -2 0  -2 2 0  2 2 0"
                            Normals="0 0 1  0 0 1  0 0 1  0 0 1" TextureCoordinates="0 1  1 1  0 0  1 0   "
                            TriangleIndices="0 1 2  1 3 2" />
                        </GeometryModel3D.Geometry>
                        <GeometryModel3D.Material>
                            <DiffuseMaterial Brush="{StaticResource ResourceKey=MyAppImageBrush}" />
                        </GeometryModel3D.Material>
                    </GeometryModel3D>
                </ModelVisual3D.Content>
            </ModelVisual3D>
        </Viewport3D>
    </Grid>
</Window>
W wyniku otrzymamy okno dokładnie takie jak poprzednio. Oczywiście można to zrobić przez ResourceDictionary (i jest to sposób zalecany przez Ms). Zaletą takiego podejścia są:
  • Mamy jedno miejsce, w którym nastąpiła deklaracja zasobu.
  • Łatwo możemy się do zasobu odwołać.
  • Jest to standardowy sposób i każdy, kto programuje w WPF, będzie wiedział jak z tego skorzystać.
  • Nie tworzymy żadnych „helperów”, bo nie ma takiej potrzeby. Nie wynajdujemy koła na nowo!!

Zasoby (resources) w assembly

Dodatkowe pliki dla aplikacji można trzymać bezpośrednio w assembly. Pisałem o tym już wcześniej w artykule: "Prosta obsługa Resource’ów aplikacji .NET (C#)". Teraz skupmy się jednak na wykorzystaniu tego w WPF. Chciałbym tutaj podkreślić, że nie jestem fanem trzymania informacji w plikach EXE i nie zalecam tam tego robić! Nie jest jednak dobrym podejściem trzymanie grafik, w taki sposób, że każda grafika (lub dowolny inny zasób) znajduje się w osobnym pliku. Przecież nie chcemy narażać się na potencjalne problemy z deployment'em – trzeba pamiętać, co było dodawane. Zastanówmy się również, czy chcemy pozwolić każdemu (nawet niedoświadczonemu) użytkownikowi na zmienianie naszej aplikacji (prawie każdy potrafi podmienić obrazek w jakimś katalogu, czy tego na prawdę chcemy?). Do zgromadzenia naszych zasobów lepiej wykorzystać odrębne assembly. A następnie można w XAML zdefiniować zasoby przy pomocy adresacji PACK.
        <BitmapImage x:Key="MyImageSource" 
         UriSource=
          "pack://application:,,,/MyResourcesLibrary;component/puzzle.png" />
Wspomniane assembly nie musi być nawet dodane do projektu jako referencja (można je podmieniać później bez rekompilacji). To jak łatwo to można zrobić pokazuje poniższy rysunek:

Literatura

Polecam również przeczytać więcej na ten temat:
Pamiętajcie, że lepiej wykorzystać coś, co ktoś już dla nas przygotował, są zasoby – korzystajmy z nich, nie wynajdujmy koła na nowo!

PS

Mam nadzieję, że nie uraziłem nikogo. Jeżeli tak, to przepraszam, ale nie taki był mój zamiar.
Promuj

2 komentarze:

  1. Wielkie dzięki.. miałem trochę kłopotu z ikonkami w programie, gdy uruchamiałem go z instalki. Trafiłem na ten wpis i wszystko stalo sie proste. Wielkie dzieki jeszcze raz :)

    OdpowiedzUsuń
  2. Co zrobić, kiedy VS2013 nie widzi mojego assembly z zasobami, tzn wyświetlanie ostrzeżenie (po najechaniu na linię z odwołaniem 'UriSource="pack://application:,,,' w xaml)
    "Could not load file or assembly 'MojeZasoby, Culture=Neutral' or one of its dependencies."?
    Zrobiłem wszystko jak na instrukcji z obrazka oprócz "Copied in Build event (not referenced)", bo nie mam pojęcia jak to zrobić.

    OdpowiedzUsuń

Posty powiązane / Related posts