Gemeinsame Icon-Bibliothek in größeren Desktop-Apps

Je umfangreicher Desktop-Apps werden, desto größer wird auch ein Problem, welches zumeist zu Beginn der Entwicklung unterschätzt wird: Wo werden Icons abgelegt, welche von verschiedenen Programmteilen verwendet werden? Viele verschiedene Programmteile und Module (und wie sie auch immer genannt werden…), bei denen jeweils lokal Icons abgelegt werden, können nach einigen Jahren stetiger Entwicklung für Chaos sorgen. Bei meinen Projekten verstärkt sich das Problem zusätzlich aufgrund der Tatsache, dass sich ältere Windows.Forms- und neuere WPF-Komponenten mischen.

Bezogen auf WPF könnte man jetzt (zu Recht) sagen, die Packet URIs [1] lösen dieses Problem für WPF-Basierte Anwendungen bereits. Sie ermöglichen es nämlich, Icons in einer gemeinsam genutzten Assembly abzulegen und von anderen Assemblies direkt aus dem Xaml-Code heraus darauf zu referenzieren. Aber ganz ehrlich: Ich persönlich bin absolut kein Fan der Syntax dahinter, da erstens teils sehr lange URI Pfade herauskommen und zweitens keine direkte Intelli Sense dafür zur Verfügung steht. Dazu kommt, warum auch immer, dass ich die Syntax stets nach einigen Tagen oder Wochen wieder vergessen habe. Gehen wir zum Beispiel von einer Assembly „CommonAssembly“ aus, welche alle gemeinsam verwendeten Icons für ein größeres Projekt enthalten soll. Abhängig der darin umgesetzten Ordnerstruktur käme etwa nachfolgende Packet URI raus.

pack://application:,,,/CommonAssembly;component/Icons/Toolbar/AddIcon_16x16.png

Meine aktuelle Alternative für dieses Problem ist es, über eine eigene MarkupExtension zu gehen. Diese MarkupExtension nimmt eine Enum als Parameter, welche jedes verfügbare Icon als Member hat. Diese Enum hat den Vorteil, dass innerhalb des Xaml-Codes Intelli Sense Unterstützung zur Verfügung steht. Das Ergebnis (also das Icon) kann man sich auch direkt im Designer anschauen, da MarkupExtensions innerhalb des Designers von Visual Studio direkt ausgeführt werden. Nachfolgend ein Beispiel aus dem ModelViewer von Seeing# [2]. Hier wird bei den ToggleButtons jeweils ein Icon über die Content-Eigenschaft zugewiesen. Die MarkupExtension heißt hier „ModelViewerIcon“, der Parameter mit der Enum einfach „Icon“. Zusätzlich gibt es hier noch den Parameter „ResultType“. Grund dafür ist die Tatsache, dass an manchen Stellen das Icon als Typ „Image“ zugewiesen werden muss und an anderen als Type „ImageSource“. Ob noch weitere Typen dazu kommen, weiß ich derzeit noch nicht – ich lass mich mal überraschen.

<ToolBarTray IsLocked="True"
             DockPanel.Dock="Top">
    <ToolBar>
        <ToggleButton IsChecked="{Binding ElementName=CtrlRenderer, Path=RenderLoop.ViewConfiguration.ShowTextures}"
                      Content="{local:ModelViewerIcon Icon=Screenshot,ResultType=Image}"
                      ToolTip="Enable/Disable Textures" />

        <ToggleButton IsChecked="{Binding ElementName=CtrlRenderer, Path=RenderLoop.ViewConfiguration.WireframeEnabled}"
                      Content="{local:ModelViewerIcon Icon=Wireframe,ResultType=Image}"
                      ToolTip="Enable/Disable Wireframe mode" />

        <Separator />

        <ToggleButton IsChecked="{Binding Path=MiscObjects.BackgroundVisible}"
                      Content="{local:ModelViewerIcon Icon=Floor, ResultType=Image}"
                      ToolTip="Enable/Disable Background" />
        <Label Content="# Tiles:" />
        <TextBox Text="{Binding Path=MiscObjects.TilesPerSide}"
                 Width="50"
                 BorderBrush="Gray" BorderThickness="1" />
    </ToolBar>
</ToolBarTray>

Aktuell ist es im ModelViewer noch so implementiert, dass sich die MarkupExtension direkt im gleichen Projekt befindet. Generell kann sich diese aber genauso in einer anderen Assembly befinden – ganz wie bei jeder anderen MarkupExtension auch. Nachfolgend noch das Coding der MarkupExtension und der Enum, um sich ein detailliertes Bild davon zu machen.

public enum ModelViewerIcon
{
    Adapter,
    Close,
    Floor,
    Help,
    Open,
    Save,
    Screenshot,
    Wireframe,
    Refresh,
    ModelEdit,
    World,
    Page
}

public enum IconResultType
{
    BitmapImage,

    Image
}

public class ModelViewerIconExtension : MarkupExtension
{
    private static Dictionary<string, BitmapImage> s_images;

    /// <summary>
    /// Initializes the <see cref="ModelViewerIconExtension"/> class.
    /// </summary>
    static ModelViewerIconExtension()
    {
        s_images = new Dictionary<string, BitmapImage>();
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ModelViewerIconExtension"/> class.
    /// </summary>
    public ModelViewerIconExtension()
    {
        this.Icon = ModelViewerIcon.Open;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        string uriPath = $"pack://application:,,,/SeeingSharpModelViewer;component/Assets/Icons/{this.Icon}_16x16.png";

        // Load the ImageSource (me may have cached it before
        BitmapImage result = null;
        Action actionCreateBitmapSource = () =>
        {
            if (!s_images.TryGetValue(uriPath, out result))
            {
                result = new BitmapImage(new Uri(uriPath, UriKind.Absolute));
                result.Freeze();
                s_images.Add(uriPath, result);
            }
        };

        // Create the result object
        switch (this.ResultType)
        {
            case IconResultType.Image:
                actionCreateBitmapSource();
                Image imgControl = new Image();
                imgControl.Source = result;
                imgControl.Width = 16.0;
                imgControl.Height = 16.0;
                return imgControl;

            case IconResultType.BitmapImage:
                actionCreateBitmapSource();
                return result;
        }

        return null;
    }

    public ModelViewerIcon Icon
    {
        get;
        set;
    }

    public IconResultType ResultType
    {
        get;
        set;
    }
}

Die Umsetzung ist, wie man sieht, relativ einfach gestaltet. In der Methode „ProvideValue“ wird das Packet URI mit dem gewünschten Icon zusammengebaut. In diesem Beispiel gehe ich der Einfachheit halber davon aus, dass es einen Ordner mit allen zur Verfügung stehenden Icons gibt. Wird die Zahl der Icons so groß, dass dieses Vorgehen unübersichtlich wird (was ja schnell passiert), wäre es ein guter Weg, mit der Unterteilung in Unterordner auch verschiedene MarkupExtensions zu entwickeln. Somit geht der Vorteil der Unterstützung der Intelli Sense nicht verloren. Auch verschiedene MarkupExtensions verteilt auf verschiedene Assemblies sind denkbar, um so je nach Komponente bzw. Zugehörigkeit zu einer Schicht andere Icons anzubieten.

Ein Nachteil dieser Lösung ist natürlich noch, dass Windows.Forms-Anwendung noch nicht berücksichtigt sind. Mein persönliches Ziel ist aber, auch diese Welt auf dem gleichen Weg abzudecken, entweder über eine eigene Ableitung der ImageList-Klasse oder über eine eigene Component, welche sich um die Zuweisung der Icons zu Steuerelementen kümmert. Im schlimmsten Fall bliebe noch das Code Behind für diese Aufgabe übrig – letzten Endes kann aber eine gemeinsame Icon-Bibliothek benutzt werden und das ist für mich das wichtigste Ziel.

Verweise
[1] = https://msdn.microsoft.com/de-de/library/aa970069(v=vs.110).aspx
[2] = https://github.com/RolandKoenig/SeeingSharp/tree/master/Tools/SeeingSharpModelViewer

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.