Einführung

In diesem Blog Eintrag wird beschrieben, wie man sich in dem Application Block „Composite Client Library“ oder PRISM eigene Adapter schreiben kann, um Controls als UI Regions zu verwenden. Dazu wird exemplarisch eine PRISM Lösung erstellt und benutzt, die die Verwendung eines solchen Adapters für das Microsoft WPF Ribbon Control in PRISM beschriebt.

Der Code, der zu Demonstationszwecken geschrieben wird, kann als lauffähige Visual Studio 2010 Lösung unter als Anhang am den Blogeintrag heruntergeladen werden.

Technische Voraussetzungen

 

Ribbon

Ein Ribbon (engl. „Band“, deutsch „Multifunktionsleiste“) ist ein grafisches Bedienkonzept für Anwendungsprogramme, das die Konzepte von Menüs und Symbolleisten miteinander verbindet. Ribbons kommen beispielsweise in Microsoft Office ab Version 2007 zum Einsatz, sowie in WordPad, Microsoft Paint unter Windows 7, in AutoCAD, Inventor und SnagIt. Das Ribbon ist eine Kombination von Menu und Toolbar und zeichnet sich nach einer Lernphase durch einfachere Bedienung von Anwendungen aus. In einem Ribbon gibt es neben Button und Imagebuttons eine Reihe weitere Controls, z.B. Textboxen, Comboboxen, Checkboxen, u.v.a.m..

Diese Abbildung zeigt ein typisches Ribbon.

image

Ein Ribbon besteht aus verschiedenen Ribbontabs. In den Ribbontabs gibt es verschiedene Ribbongruppen. In den Gruppen sind die eigentlichen Ribbon Bedienelemente dargestellt.

Weiterhin gibt es einen Application Button, hinter dem sich ein weiteres Menu versteckt. Es existiert auch ein Quick Access Toolbar.

Zentrale Ribbontabs werden immer angezeigt. Es gibt auch „contextuelle“ Tabs, die nur ein bestimmten Situationen = Context sichtbar sind. Z.B. „Format Picture“ Tab erscheint nur, wenn ein Bild selektiert ist.

Modale Tabs, Galleries und enhanced Tooltips runden die Funktionalität ab.

Eine Beschreibung an welchen Stellen sich ein Ribbon anpassen lässt und Styleguide, mit empfohlenen UI Konzepte für das Ribbon sind, gibt es unter à http://msdn.microsoft.com/en-us/library/cc872782.aspx .

Der Inhalt hinter dem Link ist auf jedem Fall lesenswert, er erklärt an Beispielen was richtige und falsche Verwendung von Ribbonelementen ist.

 

PRISM UI Komposition

PRISM ist ein Application Block mit dem sich modulare Anwendungen schreiben lassen. Das Konzept der Modularität wird von vielen, neueren Rich- und Smart Client Anwendungen in ihrer Architektur verwendet. Es ergeben sich eine ganze Reihe von unterschiedlichen Vorteilen aus der modularen Aufteilung und Trennung dieser Anwendungen.

Diese modulare Trennung besteht sowohl zwischen den fachlichen/funktionalen Modulen. Aber es existiert die Trennung auch zwischen den Userinterface des Anwendungsfensters und den UI Elementen der Views. Genauso wie Module los gekoppelt sind, möchte man auch das UI des Anwendungsrahmen und das UI der fachlichen View trennen. Der Gründe für die UI Entkopplung sind:

  • Wiederverwendung: Module, die nicht fest auf ein Anwendungsfenster fixiert sind, können in anderen Anwendungsfenstern wiederverwendet werden.
  • Geringe Abhängigkeiten: Wenn man etwas im Anwendungsfenster ändert hat, das keinen Einfluss auf die Funktionalität des Moduls.
  • Eingrenzung der optischen Beeinflussung des Anwendungsfensters durch Module. Module können nur bestimmte freigegebene Bereiche im Anwendungsfenster modifizieren.

Das Userinterface einer so aufgebauten modularen Anwendung wird durch UI Composition erreicht. UI Composition ist das Konzept, das sich das UI der Anwendung aus vielen Teilen zusammensetzt und so ein visuelles Ganzes ergibt.

Regions

Damit das in PRISM funktioniert, stellt die Anwendungsfenster „Regionen“ bereit. In diesen wohldefinierten Bereichen und nur dort können die fachlichen Module ihre Views anzeigen.

image

Abbildung 1 Beispiel eines PRISM UIs mit Regions

Die Verbindungen von Regionen und den Views aus den Modulen realisiert der RegionManager.

image

Abbildung 2 - Zusammenspiel RegionManager und Region

Für folgende WPF Controls hat PRISM vorgefertigte RegionAdapter, d.h. diese Klassen können als Regions im Anwendungsfenster definiert werden:

  • ContentControlRegionAdapter. Für die Klasse System.Windows.Controls.ContentControl und alle abgeleiteten Klassen.
  • SelectorRegionAdapter. Für die Klasse System.Windows.Controls.Primitives.Selector und alle abgeleiteten.
  • ItemsControlRegionAdapter. Für alle Controls der Klasse System.Windows.Controls.ItemsControl und abgeleitet Klassen.

Wenn es ein Control gibt, z.B. Ribbon, für das noch keine Region gibt, kann man einen RegionAdapter dafür implementieren.

UI Komposition

In PRISM gibt es 2 Arten, wie man UI Komposition technisch realisieren kann:

  • View Discovery
  • View Injection

Die View Discovery in PRISM beruht darauf, dass Module Views für eine Region registrieren. Wenn die Region angezeigt wird, wird automatisch das View Element erzeugt und angezeigt.

Die View Injection beruht auf dem Konzept, das View programmatisch zu den Regionen hinzugefügt und entfernt werden und dass die Logik dazu aus den Modulen bzw. Klassen aus den Modulen kommt.

Code – Die Anwendung

Die Lösung zur Beispielanwendung besteht aus 5 C# Projekten.

image

Abbildung 3 - Lösungsstruktur

Das Executable ist das Projekt RibbonRegionDemo. Das Projekt hat ein Anwendungsfenster und dieses Anwendungsfenster hat ein Ribbon, welches via Regionadapter von den Modulen aus genutzt werden. Das Executable wird gestartet und lädt die Module 1-3. Die Module 1-3 sind C# Projekte und keine festen Referenzen untereinander und zum Anwendungsfenster. Im Project ClientCrossCutting ist der Prism Ribbon Regionadapter implementiert.

Der Code und UI ist bewusst einfach gehalten, um nur auf den Aspekt Regionadapter zu fokussieren. Wichtige Aspekte, wie Fehlerbehandlung …, wurden bewusst vernachlässigt.

Die 3 Module greifen in ihrer IModule.Initialize Methode auf den Ribbon Regionadapter zu und etablieren eigene Ribboncontrols am UI.

In Module1 sieht die IModule.Initialize Methode so aus:

        public void Initialize()
        {
            try
            {
                System.Diagnostics.Trace.WriteLine( "Module1.ModuleInit ..." );

                WyswygRibbon ribbonWindow = new WyswygRibbon();

                regionManager.AddToRegion( "RibbonRegion", ribbonWindow.Ribbon );                
            }
            catch (Exception ex)
            {
                System.Diagnostics.Trace.WriteLine( ex.ToString() ); 
            }
        }

 

Der Code ist sehr kurz, da hier das RibbonTab, welches im Ribbon des Hauptfensters gezeigt werden soll, grafisch gestaltet werden kann.

Im Projekt gibt es also eine XAML Window, das nur dazu dient, die grafische Gestaltung des Ribbons zuzulassen. Mit der Methode RegionManager.AddToRegion wird dann das Ribbon aus dem XAML Manager an den RegionAdapter übergeben, der das RibbonTab im Ribbon des Hauptfenster anzeigt.

In Module2 wird in der IModule.Initialize Methode werden die erforderlichen Ribboncontrols programmatisch angelegt und dann wird das RibbonTab als Ankercontrol an den RegionManager übergeben.

 
   public void Initialize()
        {
            try
            {               
                System.Diagnostics.Trace.WriteLine( "Module2.ModuleInit ..." );
              
                ribbonTab = new RibbonTab();
                ribbonTab.Header = "Module 2 RibbonTab";
                RibbonGroup ribbonGroup = new RibbonGroup();
                RibbonButton ribbonButton = new RibbonButton();
                ribbonButton.Label = "Klick me!";
                ribbonButton.Width = 100;
                ribbonButton.Height = 30;
                ribbonButton.Click += new System.Windows.RoutedEventHandler( rb_Click );
                ribbonGroup.Items.Add( ribbonButton );
                ribbonTab.Items.Add( ribbonGroup );
                
                foreach (Region r in regionManager.Regions)
                {
                    System.Diagnostics.Trace.WriteLine( "Region=" + r ); 
                }

                regionManager.AddToRegion( "RibbonRegion", ribbonTab );             
            }
            catch (Exception ex)
            {
                System.Diagnostics.Trace.WriteLine( ex ); 
            }
        }


 

In Module3 in der IModule.Initialize Methoden werden nur RibbonButtons erzeugt. Die im der ersten Ribbongroup des ersten Ribbontabs angezeigt werden. Annahme ist hier das es also im Ribbon des Hauptfensters mindestens 1 Ribbontab mit einer Ribbongroup gibt.

        public void Initialize()
        {
            try
            {
                System.Diagnostics.Trace.WriteLine( "Module3.ModuleInit ..." );

                ribbonButton1 = new RibbonButton();
                ribbonButton1.Height = 30;
                ribbonButton1.Width = 100;
                ribbonButton1.Label = "m3 button";

                regionManager.AddToRegion( "RibbonRegion", ribbonButton1 );

                ribbonButton = new RibbonButton();
                ribbonButton.Height = 30;
                ribbonButton.Width = 100;
                ribbonButton.Label = "Remove m3 button";
                ribbonButton.Click += new RoutedEventHandler(rb_Click);

                regionManager.AddToRegion( "RibbonRegion", ribbonButton );            
            }
            catch (Exception ex)
            {
                System.Diagnostics.Trace.WriteLine( ex );
            }
        }

Man sieht also an dem kurzen Beispielcode, dass es sich um Standardprismcode wandelt. Das eigentliche Ribbonhandling passiert in dem Ribbon RegionAdapter selbst.

 

CODE - RegionAdapter

In der Methode RegionAdapterBase.Adapt wird das Adapterribbon von der Factory übergeben. Diese Methode wird einmal aus dem Bootstrapper aufgerufen. Das Ribbon Regionadapter muss logischerweise dem PRISM Framework bekanntgegeben werden.

        protected override Microsoft.Practices.Prism.Regions.RegionAdapterMappings ConfigureRegionAdapterMappings()
        {
            var regionAdapterMappings = Container.TryResolve<RegionAdapterMappings>();
            if (regionAdapterMappings != null)
            {
                regionAdapterMappings.RegisterMapping( typeof( Ribbon ), this.Container.Resolve<RibbonRegionAdapter>() );
            }

            return base.ConfigureRegionAdapterMappings();
        }

Aus dieser Sequenz wird dann einmal die Methode Adapt aufgerufen.

 
        protected override void Adapt( IRegion region, Ribbon regionTarget )
        {
            ribbonRegionTarget = regionTarget;

            region.ActiveViews.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler( OnActiveViewsChanged );

            foreach (RibbonTab ribbonTabView in region.ActiveViews)
            {
                regionTarget.Items.Add( ribbonTabView );
            }
        }

 

Die eigentliche Logik des Hinzufügen und Entfernens von Ribboncontrols zur Ribbonregion erfolgt in der Methode RegionAdapterBase.OnActiveViewsChanged. Diese Methode wird aufgerufen, wenn im Code RegionManager.AddToRegion oder Region.Remove aufgerufen wird.

        private void OnActiveViewsChanged( object sender, NotifyCollectionChangedEventArgs e )
        {
            switch (e.Action)
            {
                // Hinzufügen von Ribboncontrols
                case NotifyCollectionChangedAction.Add:
                    // für jedes neue Item
                    foreach (object ribbonView in e.NewItems)
                    {
                        // wenn es ein Ribbon ist - SPEZIALFALL
                        if (ribbonView is Ribbon)
                        {
                            Ribbon rb = ribbonView as Ribbon;
                            for (int i = rb.Items.Count - 1; i >= 0; i--)
                            {
                                if (rb.Items[i] is RibbonTab)
                                {
                                    // Umhängen aller Ribbontab in das Ribbon des Adapters
                                    RibbonTab rt = (rb.Items[i] as RibbonTab);
                                    rb.Items.Remove( rt );
                                    ribbonRegionTarget.Items.Add( rt );
                                }
                            }
                        }
                        else
                            // wenn es ein Ribbontab ist
                            if (ribbonView is RibbonTab)
                            {
                                ribbonRegionTarget.Items.Add( ribbonView );
                            }
                            else
                                // wenn es ein Ribbonbutton ist
                                if (ribbonView is RibbonButton)
                                {
                                    bool alreadyInserted = false;

                                    foreach (object ot in ribbonRegionTarget.Items)
                                    {
                                        // in das erste Ribbontab
                                        if (ot is RibbonTab && !alreadyInserted)
                                        {
                                            foreach (object og in ((RibbonTab)ot).Items)
                                            {
                                                // in die erste Ribbon group
                                                if (og is RibbonGroup)
                                                {
                                                    ((RibbonGroup)og).Items.Add( ribbonView );
                                                    alreadyInserted = true;
                                                    break;
                                                }
                                            }
                                        }
                                    }
                                }
                                else
                                {
                                    throw new ArgumentException( "unsupported type " + ribbonView.GetType().Name );
                                }
                    }
                    break;
                // entfernen von Ribboncontrols 
                case NotifyCollectionChangedAction.Remove:
                    {
                        if (e.NewItems != null)
                            foreach (object ribbonView in e.NewItems)
                            {
                                // wenn es ein Ribbon ist - SPEZIALFALL
                                if (ribbonView is Ribbon)
                                {
                                    Ribbon rb = ribbonView as Ribbon;
                                    for (int i = rb.Items.Count - 1; i >= 0; i--)
                                    {
                                        if (rb.Items[i] is RibbonTab)
                                        {
                                            RibbonTab rt = (rb.Items[i] as RibbonTab);
                                            rb.Items.Remove( rt );
                                            ribbonRegionTarget.Items.Remove( rt );
                                        }
                                    }
                                }
                                else
                                    // wenn es ein Ribbontab ist
                                    if (ribbonView is RibbonTab)
                                    {
                                        ribbonRegionTarget.Items.Remove( ribbonView );
                                    }
                                    else
                                        // wenn es ein Ribbonbutton ist
                                        if (ribbonView is RibbonButton)
                                        {
                                            // doesn't work - strange!
                                            ((RibbonButton)ribbonView).Visibility = System.Windows.Visibility.Hidden;
                                            // doesn't work - strange!

                                            ribbonRegionTarget.Items.Remove( ribbonView );
                                        }
                                        else
                                        {
                                            throw new ArgumentException( "unsupported type " + ribbonView.GetType().Name );
                                        }
                            }
                        break;
                    }
            }
        }

Zusammenfassung

Das ganze Konzept ist nach der Erklärung einfach zu verstehen. Das vorgestellte Ribbon RegionAdapter ist eine exemplarische Implementierung und nicht funktional vollständig. Es fehlen noch Konzepte für das Quick Access Menu und für das ApplicationButton Menu.