Synchroniseren van de Ribbon met een TaskPane in Word 2007
Het maken van een TaskPane in Word 2007 met een bijbehorende Ribbon knop om de TaskPane zichtbaar te maken is iets dat niet moeilijk is met de laatste versie van Visual Studio Tools for Office (VSTO). De eerste stappen waarbij een klik op de ribbon knop de TaskPane zichtbaar maakt zijn snel gezet. Helaas blijkt het toch wat lastiger om deze twee echt goed samen te laten werken.
Het probleem
Laten we eerst eens kijken wat nu eigenlijk het probleem is. De bron van het probleem zit in het feit dat Word 2007 een zogenaamde Single Document Interface (SDI) heeft. Dat wil zeggen dat er voor elk document dat geopend wordt een nieuw Word venster geopend wordt om het document in te tonen. Aangezien een TaskPane in een Word venster getoond wordt, moet er voor elk venster een apart TaskPane object gemaakt worden. De Ribbon knop is ook zichtbaar bovenaan elk venster maar dit is elke keer een weergave van dezelfde Ribbon. Er is dus maar één Ribbon knop en er zijn meerdere TaskPanes. Op zich is dit niet direct een probleem. De ribbon moet gewoon op de hoogte zijn van wat het actieve venster is en wat de status van de TaskPane daarbinnen is.

Fig. 1: De Word Afsluiten dialoog
Tot zover niet direct iets spannends maar er zit toch een addertje onder het gras. En dit addertje komt in de vorm van de Office events, of beter gezegd het moment van afvuren hiervan. Zo heeft een Word document een close event. Dit event komt echter voordat de gebruiker de dialoog krijgt met de vraag of het huidige document bewaard moet worden. Eén van de opties op deze dialoog is echter Annuleren en als de gebruiker dat kiest wordt het document helemaal niet gesloten. Aangezien er na deze dialoog geen ander event komt, betekent dit dat er geen betrouwbaar event is die het afsluiten van een document weergeeft.
De oplossing
Om dit probleem in een herbruikbare manier op te lossen heb ik een TaskPanesManager klasse geschreven die de coördinatie tussen Ribbon en TaskPane en het afsluiten van documenten oplost. Het grootste deel van de oplossing zit in deze herbruikbare klasse die in elke VSTO applicatie zo gebruikt kan worden. Het enige dat in de specifieke VSTO addin gedaan moet worden is een instantie van deze TaskPanesManager maken, iets wat ik normaal gesproken in de ThisAddIn klasse doe.
Private _taskPanesManager As TaskPanesManager
Public ReadOnly Property TaskPanesManager()_
As TaskPanesManager
Get
If _taskPanesManager Is Nothing Then
_taskPanesManager = New TaskPanesManager()
End If
Return _taskPanesManager
End Get
End Property
Listing 1: Het aanmaken van de TaskPanesManager
Nadat de TaskPanesManager aangemaakt is kunnen we die vanuit de Ribbon aanroepen om individuele TaskPanes zichtbaar of onzichtbaar te maken. Dit laatste gebeurt door de Show functie van de TaskPanesManager aan te roepen met onder meer het type TaskPane dat gebruikt moet worden en het venster waar deze in getoond moet worden. Er worden hierbij automatisch event handlers toegevoegd op die punten waar dat nodig is zonder dat er enig extra werk moet gebeuren.
Public Sub OnToggleButton1(_
ByVal control As Office.IRibbonControl,_
ByVal isPressed As Boolean)
Dim theWindow As Window =_
Globals.ThisAddIn.Application.ActiveWindow
If isPressed Then
' Show the taskpane
Globals.ThisAddIn.TaskPanesManager.Show(_
GetType(UserControl1), "Demo", theWindow, control)
Else
' Hide the taskpane
Globals.ThisAddIn.TaskPanesManager.Hide(_
GetType(UserControl1), theWindow)
End If
End Sub
Listing 2: De OnToggle button van de Ribbon button waar de TaskPane zichtbaar gemaakt wordt
Om te weten of een TaskPane al dan niet zichtbaar is, en de Ribbon knop dus ingedrukt moet zijn of niet, kunnen we voor de Ribbon knop ook het GetPressed event afhandelen. Hiervoor zijn maar twee regels code nodig zoals in listing 3 aangegeven is.
Public Function GetPressed(_
ByVal control As Office.IRibbonControl) As Boolean
Dim theWindow As Window =_
Globals.ThisAddIn.Application.ActiveWindow
' Check if the taskpane is visible at the moment
Return Globals.ThisAddIn.TaskPanesManager.IsVisible(_
GetType(UserControl1), theWindow)
End Function
Listing 3: De controle of een Ribbon button ingedrukt moet zijn of niet
Om er voor te zorgen dat de TaskPanesManager de Ribbon knop kan verversen op het moment dat dit nodig is heeft de TaskPanesManager een referentie naar de Ribbon nodig. Indien deze laatste stap niet uitgevoerd wordt, zal de TaskPanesManager verder gewoon nog werken maar zal de Ribbon knop niet meer automatisch bijgewerkt worden. Deze referentie kan bijvoorbeeld in de OnLoad functie van het Ribbon object gezet worden zoals ik in listing 4 doe.
Public Sub OnLoad(ByVal ribbonUI As Office.IRibbonUI)
Me.ribbon = ribbonUI
' Add this to enable the taskpane manager to update
‘ the ribbon when a taskpane is closed
Globals.ThisAddIn.TaskPanesManager.Ribbon = ribbonUI
End Sub
Listing 4: Het koppelen van de Ribbon aan de TaskPanesManager
Met deze stappen, en uiteraard het maken van een Ribbon button en een UserControl, zijn we klaar om aan de slag te gaan.
Achter de schermen
Nu ik heb laten zien hoe de TaskPanesManager gebruikt kan worden is het misschien ook leuk om even te kijken wat deze nu eigenlijk achter de schermen doet.
Het eerste wat er in de constructor gebeurt is een event handler aan het VSTO applicatie DocumentChange event te koppelen. Dit event neemt de plaats in van het, zoals reeds besproken, niet bruikbare document Close event. In deze event handler roepen we de Cleanup functie aan - die overigens van nog meer plaatsen aangeroepen wordt - waarin we kijken of er TaskPanes zijn waarvan het venster niet meer aanwezig is. Als het venster niet aanwezig is moet dit door de gebruiker gesloten zijn en kan de TaskPane nooit meer zichtbaar worden zodat we hem het best kunnen verwijderen. Indien we een TaskPane niet verwijderen zouden ze zolang Word actief is in geheugen blijven aangezien Word en VSTO deze nooit zelf verwijderen. Nadat we alle TaskPanes die overbodig zijn verwijderd hebben roepen we de Invalidate functie van de Ribbon aan zodat alle knoppen zichzelf verversen.
Als er een TaskPane zichtbaar gemaakt moet worden kan dat via de Show functie. Van deze Show functie zijn verschillende overloads aanwezig. De werking varieert afhankelijk van welke versie van Show aangeroepen wordt maar uiteindelijk doen ze allemaal dezelfde en belangrijkste actie die bestaat uit het controleren of de combinatie van venster en TaskPane klasse al bestaat; zo ja dan deze bestaande zichtbaar maken en zo niet deze aanmaken en toevoegen. Een belangrijke actie hierbij is om een event handler toe te voegen aan het VisibleChanged event. Dit VisibleChanged event gaat af als de gebruiker de sluit knop rechts bovenaan de TaskPane gebruikt om de TaskPane te sluiten. In deze event handler zorgen we er dan voor dat de Ribbon knop ook van status verandert.
De TaskPane manager bevat ook een Hide functie die gebruikt kan worden om een specifieke TaskPane weer onzichtbaar te maken vanuit de Ribbon knop. In deze functie wordt eerst de Find functie aangeroepen die de bewuste TaskPane opzoekt. In de Find functie wordt eerst de Cleanup functie weer aangeroepen om te zorgen dat er geen oude referenties blijven hangen. Eigenlijk zou dit hier niet nodig moeten zijn aangezien dat in de DocumentChangeHandler al gebeurt, maar op deze manier is er minder fout afhandelingscode nodig in de Find functie wat de code beter leesbaar houdt.
Naast de Show en de Hide functies is er ook de eerder besproken IsVisible functie aanwezig die aangeeft of een bepaalde TaskPane in het opgegeven venster zichtbaar is.
Imports Core = Microsoft.Office.Core
Imports Microsoft.Office.Interop.Word
Imports Microsoft.Office.Tools
Public Class TaskPanesManager
' Keep track of controls associated with
' a specific TaskPane
Private _controls As New _
Collections.Generic.Dictionary(Of CustomTaskPane, String)
Public Sub New()
AddHandler _
Globals.ThisAddIn.Application.DocumentChange,_
AddressOf DocumentChangeHandler
End Sub
' Needed to update a button status when
' the TaskPane is closed
Private _ribbon As Core.IRibbonUI
Public Property Ribbon() As Core.IRibbonUI
Get
Return _ribbon
End Get
Set(ByVal value As Core.IRibbonUI)
_ribbon = value
End Set
End Property
' Show a task pane of the specified type and
' link it to the specified ribbon control
Public Function Show(ByVal theType As Type, _
ByVal caption As String, ByVal window As Window,_
ByVal control As Core.IRibbonControl)_
As CustomTaskPane
Dim taskPane As CustomTaskPane = _
Show(theType, caption, window, control.Id)
Return taskPane
End Function
' Show a TaskPane of the specified type and
' link it to the specified ribbon control
Public Function Show(ByVal theType As Type, _
ByVal caption As String, ByVal window As Window, _
ByVal controlID As String) As CustomTaskPane
Dim taskPane As CustomTaskPane = _
Show(theType, caption, window)
' Save the control ID so we can refresh it
If _controls.ContainsKey(taskPane) Then
_controls(taskPane) = controlID
Else
_controls.Add(taskPane, controlID)
End If
Return taskPane
End Function
' Show a TaskPane of the specified type
Public Function Show(ByVal theType As Type, _
ByVal caption As String, _
ByVal window As Window) As CustomTaskPane
' Try and locate the TaskPane
Dim taskPane As CustomTaskPane = _
Find(theType, window)
If taskPane Is Nothing Then
Dim ctl As UserControl = _
CType(Activator.CreateInstance(theType),_
UserControl)
taskPane = Globals.ThisAddIn.CustomTaskPanes.Add(_
ctl, caption, window)
' Add an event handler so we know when the
' user closes the TaskPane directly
AddHandler taskPane.VisibleChanged, _
AddressOf TaskPaneVisibleChangedHandler
End If
' Make the TaskPane visible
taskPane.Visible = True
Return taskPane
End Function
Public Function Hide(ByVal theType As Type, _
ByVal window As Window) As CustomTaskPane
' Try and locate the TaskPane
Dim taskPane As CustomTaskPane = Find(theType, window)
If taskPane IsNot Nothing Then
' Make the TaskPane invisible
taskPane.Visible = False
End If
Return taskPane
End Function
Public Function IsVisible(ByVal theType As Type, _
ByVal window As Window) As Boolean
Dim visible As Boolean = False
' Try and locate the TaskPane
Dim taskPane As CustomTaskPane = Find(theType, window)
If taskPane IsNot Nothing Then
' Found a TaskPane, check its visibility status
visible = taskPane.Visible
End If
Return visible
End Function
Public Function Find(ByVal theType As Type, _
ByVal window As Window) As CustomTaskPane
Dim taskPane As CustomTaskPane = Nothing
' Remove all dead TaskPane references
Cleanup()
For Each current As CustomTaskPane In _
Globals.ThisAddIn.CustomTaskPanes
If current.Control.GetType() Is theType AndAlso _
current.Window.Equals(window) Then
' Found the one we are looking for
taskPane = current
Exit For
End If
Next
Return taskPane
End Function
Public Sub Cleanup()
Dim toRemove As _
New System.Collections.Generic.Queue(_
Of CustomTaskPane)
For Each current As CustomTaskPane _
In Globals.ThisAddIn.CustomTaskPanes
Try
If current.Window Is Nothing Then
' The window has been removed, remove it
toRemove.Enqueue(current)
End If
Catch
' Something wrong with this TaskPane, remove it
toRemove.Enqueue(current)
End Try
Next
While toRemove.Count > 0
' Remove all unwanted taskpanes
Dim taskPane As CustomTaskPane=toRemove.Dequeue()
' Remove it from the taskpanes
Globals.ThisAddIn.CustomTaskPanes.Remove(taskPane)
If _controls.ContainsKey(taskPane) Then
' Remove it from the control list
_controls.Remove(taskPane)
End If
End While
End Sub
Private Sub DocumentChangeHandler()
Dim count As Integer = _
Globals.ThisAddIn.CustomTaskPanes.Count
Cleanup()
If Ribbon IsNot Nothing AndAlso _
count <> Globals.ThisAddIn.CustomTaskPanes.Count_
Then
' Something was cleaned, make sure
' the ribbon is up to date
Ribbon.Invalidate()
End If
End Sub
Private Sub TaskPaneVisibleChangedHandler(_
ByVal sender As Object, ByVal e As EventArgs)
If Ribbon IsNot Nothing Then
Dim taskPane As CustomTaskPane =_
CType(sender, CustomTaskPane)
Dim controlId As String = ""
If _controls.TryGetValue(taskPane, controlId) Then
' This TaskPane is bound to a specific control,
' just have that one repainted
Ribbon.InvalidateControl(controlId)
Else
' Be save and just repaint all ribbon controls
Ribbon.Invalidate()
End If
End If
End Sub
End Class
Listing 5: De TaskPanesManager klasse.
Conclusie
Visual Studio Tools for Office is een mooi platform om vlot applicaties te kunnen ontwikkelen die meestal voor eindgebruikers makkelijk te gebruiken zijn. Helaas is het object model van Office niet helemaal ontworpen voor dit doel en zijn er soms wat tekortkomingen. Met het gebruik van een standaard klasse zoals hierboven beschreven is dit echter nauwelijks een beperking te noemen en wordt het werken met Visual Studio Tools for Office zeer makkelijk.
Sources
De sources die bij dit artikel horen kun je downloaden via Beijer_TaskPaneRibbon_SRC.zip.