PDA

View Full Version : Best Practices: Dynamic Ribbons



Frosty
02-17-2011, 01:45 PM
EDIT: Sorry: this is Word 2010, although I think it probably applies to 2007 as well. Perhaps this should be in the Ribbon area, rather than the Word area.

I'm a pretty experienced coder, but really just making the foray into the 2007/2010 type environment in the past year. In this next year, my firm will be moving to Word 2010, and I'm doing a lot of proof of concept research right now.

What are the best practices you have worked out for creating/adjusting the ribbon?

I know you can:
1. Hard-code all of the various attributes to your custom ribbon controls into the xml...

2. Or use a series of callbacks (getLabel, getImage, etc) to dynamically assign values to various properties.

But how do you organize your data? How do you quickly add a button, remove a button?

I'm familiar with the Custom UI Editor for Office, and am thankful someone programmed it over at openxml.org.

But I've currently settled on a system where basically the only data I store in the xml is the button name (which has to be unique), and then everything else about the buttons I've worked out to be dynamic (even custom images-- see another thread I did).

And I'm currently storing my ribbon data in a custom class called RibbonControls, which has the following properties:
ArrayControlsInfo
ControlType
Exists
GetInfo
ID (this is the default)
Image
ImageUsingCustom
Label
OnAction
ScreenTip
Size
SuperTip
Visible

And the following methods
ArrayRebuild
Class_Initialize

I have a public enum that looks like this:

Public Enum RibbonControlInfo
binfid
binfLabel
binfSize
binfImage
binfImageMso
binfOnAction
binfScreentip
binfSupertip
binfVisible
binfControlType
[_HIGH]
End Enum
This allows me to set up my private array of ribbon control data so that it looks something like the following:

'if we add to our number of elements in the enum, this should automatically
'take care of making sure our multi-dimensional array is the right size
iNumElements = [_HIGH] - 1

'our first one for showing what array we're looking at in the watch
'note, any elements not entered will be defaults or emptry strings, obviously
ReDim Preserve m_aryButtonInfo(iNumElements, i) As String
m_aryButtonInfo(binfid, i) = "Button IDs"
m_aryButtonInfo(binfLabel, i) = "Labels"
m_aryButtonInfo(binfOnAction, i) = "OnActions"
m_aryButtonInfo(binfSize, i) = "Sizes"
m_aryButtonInfo(binfImage, i) = "Custom Images"
m_aryButtonInfo(binfImageMso, i) = "ImageMsos"
m_aryButtonInfo(binfScreentip, i) = "Custom Screen Tips"
m_aryButtonInfo(binfSupertip, i) = "Custom Super Tips"
m_aryButtonInfo(binfVisible, i) = "Starting Visible value"
'do we need this info? Only if we don't use the right prefix...
m_aryButtonInfo(binfControlType, i) = "Control Types"
And then each time I add a new control, I simply use


i = i + 1
ReDim Preserve m_aryButtonInfo(iNumElements, i) as String
m_aryButtonInfo(binfid, i) = "butMyButton"
etc etc...

When all is said and done, my callbacks look something like this:


Public Sub getLabel(control as IRibbonControl, ByRef ReturnedVal)
ReturnedVal = myRibbonControls(control.ID).Label
End Sub

Which I'm a fan of, simplicity-wise. It allows me to sort of have generic callbacks, a generic class, and the array is really the main respository for anything custom (the getOnAction has to be a little more klugey, since I'm not a fan of using Application.Run on a string, and instead use a Case statement to turn "UI_HelloWorld" into UI_HelloWorld in the code module.

I've also programmed ways of extracting info from the array into a good XML format, as well as a good VBA format.

This all works, but it's still an array, and not super user-friendly to adjust. And I do wonder if I'm just adding a layer of complexity because I'm very familiar with programming VB/VBA and not so familiar with XML (wouldn't be the first time I've made something more complex than it needed to be, because I didn't understand something simple).

But I'm also mystified that I'm essentially creating a class for accessing my custom Ribbon info when Microsoft probably should have done this already.

Was looking for some thoughts for how others structure their code in a flexible yet robust way.

Thanks for any input.

- Jason

gmaxey
02-19-2011, 04:42 AM
Jason,

You are far more experience at coding than I am. I have been cobbling together code to suit my own purposes and to help others in support forums like these for over a decade but I have rarely used Class modules. I admit that when I have and after I have pulled clumps of hair out in the process, they then seem to then make sense and have advantage over the more clunky methods that I typically use. Still, and I suppose that it simply lack of regular use, the steps to setup the complex, to me, Get and Let property procedures makes me avoid them whenever I can.

I saw you other post on getImage as well. I am not that familiar with the skill level of others monitoring this group, but I feel that you might have a better chance for meaningful input if you would post an example document that actually illustrates what it is that you have done. One where a reviewer could open the code modules and actually see (step through the code) how you use your class module.

Thanks.

BTW, does your class module also build and supply the getContent attribute for dynamic menu controls?

Frosty
02-19-2011, 11:43 AM
I don't know that I am a more experienced coder, but thanks for the compliment. I'm going to take a little more time and make a proper reply which answers all of the above, but I'll first just say this:

Despite not having any formal training in programming, I learned during the dotcom bubble from people who were, for lack of a better term, "real" programmers. I have, however, managed to come to an "agreement" with using classes so that we get along fairly well, even if we're not necessarily in love.

I see a lot of formally trained programmers do that whole Let/Get thing, even when it's not necessary and/or adds nothing.

So just a quick primer on my typical approach, before I get to the real nuts and bolt in a later post where I actually upload a macro template with the whole ribbon class (since there will be a lot of code to that).

For me, classes are just a way of organizing my thoughts. I was taught to separate the "data" from the "doings" -- not always possible, but always a goal.

I'll illustrate my main point with two simplified versions of classes I typically use:
FirmPerson
FirmInfo

The FirmInfo class is complicated and holds a lot of different stuff, but for this purpose (showing that classes don't have to be exercises in hair pulling, even in the initial creation), we only care about one property, the People property, which is a collection (of FirmPerson class objects).

Getting away from all of the nitty gritty (which I'll get to in my later post), it seems like every example of classes you see on the web would have the FirmPerson class look like the following:

FirmPerson class (typical demo)

Option Explicit

Private m_sFirstName As String
Private m_sLastName As String
Private m_sMiddleName As String
'----------------------------------------------------------------------------------------------
Public Property Get FirstName() As String
FirstName = m_sFirstName
End Property
'----------------------------------------------------------------------------------------------
Public Property Let FirstName(sFirstName As String)
m_sFirstName = sFirstName
End Property
'----------------------------------------------------------------------------------------------
Public Property Get MiddleName() As String
MiddleName = m_sMiddleName
End Property
'----------------------------------------------------------------------------------------------
Public Property Let MiddleName(sMiddleName As String)
m_sMiddleName = sMiddleName
End Property
'----------------------------------------------------------------------------------------------
Public Property Get LastName() As String
LastName = m_sLastName
End Property
'----------------------------------------------------------------------------------------------
Public Property Let LastName(sLastName As String)
m_sLastName = sLastName
End Property
'----------------------------------------------------------------------------------------------
Public Property Get FullName() As String
Dim sTemp As String
'first name
sTemp = Trim(Me.FirstName) & " "
'middle name, if any
If Trim(Me.MiddleName) <> "" Then
sTemp = sTemp & Trim(Me.MiddleName) & " "
End If
'last name
sTemp = sTemp & Trim(Me.LastName)
'in case we have empty names
sTemp = Trim(sTemp)
FullName = sTemp
End Property

(I'm not totally crazy about this vba parser, as it doesn't make my code look like my code, but I do love the pretty colors)

And what I utilize is more along the lines of the following:
FirmPerson class

Option Explicit

Public FirstName As String
Public LastName As String
Public MiddleName As String
'----------------------------------------------------------------------------------------------
Public Property Get FullName() As String
Dim sTemp As String
'first name
sTemp = Trim(Me.FirstName) & " "
'middle name, if any
If Trim(Me.MiddleName) <> "" Then
sTemp = sTemp & Trim(Me.MiddleName) & " "
End If
'last name
sTemp = sTemp & Trim(Me.LastName)
'in case we have empty names
sTemp = Trim(sTemp)
FullName = sTemp
End Property

Since most of the stuff is just a read/write property with no validation, I can get away with just having public variables. Although the derived read-only property (FullName), obviously requires coding.

Maybe this is a "classes for dummies" type post. But I've had this conversation with a couple of other good programmers recently, and I think I've helped them with the way I think about classes.

Using the simplified FirmPerson Class, this is then a simplified version of my FirmInfo class (I'll hard code a couple of people, since how you get the people is a different concept)
FirmInfo class

Option Explicit
Option Base 1

Private m_People As Collection
Private m_aryPeople() As String
'-----------------------------------------------------------------------------------------------
' The people at the firm
'-----------------------------------------------------------------------------------------------
Public Property Get People() As Collection
'check our private collection
If m_People Is Nothing Then
GetSomeFakePeople
End If
Set People = m_People
End Property
'-----------------------------------------------------------------------------------------------
' get some people, quick and dirty like, just for purposes of the demo
'-----------------------------------------------------------------------------------------------
Private Sub GetSomeFakePeople()
Dim oMyPerson As FirmPerson

'create a new instance of our private people collection
Set m_People = New Collection

'get a new instance of a person
Set oMyPerson = New FirmPerson
'set some values
oMyPerson.FirstName = "John"
oMyPerson.MiddleName = "Jacob"
oMyPerson.LastName = "Smithers"
'add to our collection of people
m_People.Add oMyPerson
'release the memory
Set oMyPerson = Nothing

'rinse and repeat
Set oMyPerson = New FirmPerson
'set some values
oMyPerson.FirstName = "Jane"
oMyPerson.MiddleName = "Jinglemeyer"
oMyPerson.LastName = "Smith"
'add to our collection of people
m_People.Add oMyPerson
'release the memory
Set oMyPerson = Nothing

End Sub
'-----------------------------------------------------------------------------------------------
' Pass in a name, see if the name exists
'-----------------------------------------------------------------------------------------------
Public Property Get PersonExists(sFullName As String) As Boolean
Dim i As Integer
Dim bRet As Boolean

On Error GoTo l_err

'go through the index to find a match (and verify using the FullName as a Key
For i = 1 To m_People.count
'if we find a match, then we will be able to reference it
If sFullName = m_People(i).FullName Then
bRet = True
Exit For
End If
Next

l_exit:
PersonExists = bRet
Exit Property
l_err:
bRet = False
Resume l_exit
End Property
'-----------------------------------------------------------------------------------------------
' for easily adding to combo/dropdown userform ontrols using .List property
'-----------------------------------------------------------------------------------------------
Public Property Get Array_PeopleFullNames() As Variant
Dim i As Integer
i = Me.People.count
ReDim Preserve m_aryPeople(i)
For i = 1 To Me.People.count
m_aryPeople(i) = Me.People(i).FullName
Next
Array_PeopleFullNames = m_aryPeople
End Property

And then the last piece is a regular module which has the following (this allows me to access my class all the time, although it may not be appropriate in all cases of class uses, since this is essentially using my class as a global variable, rather than a repeatable organization structure--however, this concept will also be useful for the Ribbon discussion in the next post):

Option Explicit
Public pub_FirmInfo As FirmInfo
'-----------------------------------------------------------------------------------------------
' Publically accessible version of our firm info class
'-----------------------------------------------------------------------------------------------
Public Function myFirmInfo() As FirmInfo
If pub_FirmInfo Is Nothing Then
Set pub_FirmInfo = New FirmInfo
End If
Set myFirmInfo = pub_FirmInfo
End Function

I think it helps to use the watch (both locals and regular) windows when you're playing around with classes.

For me, they are just ways of organizing my own thoughts... so that when I come back to the code later, if I've named my stuff well, I don't really have to read the code to figure out what it's supposed to do.

This was longer than I meant, but I wanted to lay out why I was looking at a class for the Ribbon Controls, so it sort of contextually makes sense. I think.

In answer to one of your other questions: no, it doesn't dynamically load content yet, because this class is still very much a work in progress. I've read enough to know a lot of what I can do in terms of Ribbon stuff... it's more about having some people who have more experience weigh in on design architecture.

If someone who understands what I'm doing says "that's all well and good, but I think you just want to do most of this customization you're doing in the XML, and leave the rest of it to a one-off type thing in vba" then that would be great to know.

At the same time, if I help someone who is struggling to work with the Ribbon by showing my way of organizing my thoughts... that'd be great.

Thanks for reading.

- Jason aka "Frosty"

p.s. Greg: if you (or anyone, really) wants to adapt and change this or any of my code for the purposes of creating another of your excellent tips, please feel free. Creating tips for other people is not something I'm in the habit of, so I'm sure you could create something more simple and coherent than I can.

gmaxey
02-19-2011, 07:46 PM
Jason,

Try as I might I can't get your demo to work. I try:

Sub Test()
MsgBox myFirmInfo.PersonExists("John Jacob Smithers")
End Sub

and it returns false (due to your error handling). I have probably not initialized the class or something. I use these things so rarely I that every time I do I seem to start over again.

If after I see your next post with the working example and I can get my head around the idea I will be glad to try to publish it. It might take awhile because I have so much else (unrelated to Word or programing) going on right now.

Thanks for sharing.

gmaxey
02-19-2011, 07:58 PM
Not sure if it is the right way or your intended way, but if I add call to:

GetSomeFakePeople

befor your:

On Error GoTo l_err

I can get my Msgbox text to work.


I don't know that I am a more experienced coder, but thanks for the compliment. I'm going to take a little more time and make a proper reply which answers all of the above, but I'll first just say this:

Despite not having any formal training in programming, I learned during the dotcom bubble from people who were, for lack of a better term, "real" programmers. I have, however, managed to come to an "agreement" with using classes so that we get along fairly well, even if we're not necessarily in love.

I see a lot of formally trained programmers do that whole Let/Get thing, even when it's not necessary and/or adds nothing.

So just a quick primer on my typical approach, before I get to the real nuts and bolt in a later post where I actually upload a macro template with the whole ribbon class (since there will be a lot of code to that).

For me, classes are just a way of organizing my thoughts. I was taught to separate the "data" from the "doings" -- not always possible, but always a goal.

I'll illustrate my main point with two simplified versions of classes I typically use:
FirmPerson
FirmInfo

The FirmInfo class is complicated and holds a lot of different stuff, but for this purpose (showing that classes don't have to be exercises in hair pulling, even in the initial creation), we only care about one property, the People property, which is a collection (of FirmPerson class objects).

Getting away from all of the nitty gritty (which I'll get to in my later post), it seems like every example of classes you see on the web would have the FirmPerson class look like the following:

FirmPerson class (typical demo)

Option Explicit

Private m_sFirstName As String
Private m_sLastName As String
Private m_sMiddleName As String
'----------------------------------------------------------------------------------------------
Public Property Get FirstName() As String
FirstName = m_sFirstName
End Property
'----------------------------------------------------------------------------------------------
Public Property Let FirstName(sFirstName As String)
m_sFirstName = sFirstName
End Property
'----------------------------------------------------------------------------------------------
Public Property Get MiddleName() As String
MiddleName = m_sMiddleName
End Property
'----------------------------------------------------------------------------------------------
Public Property Let MiddleName(sMiddleName As String)
m_sMiddleName = sMiddleName
End Property
'----------------------------------------------------------------------------------------------
Public Property Get LastName() As String
LastName = m_sLastName
End Property
'----------------------------------------------------------------------------------------------
Public Property Let LastName(sLastName As String)
m_sLastName = sLastName
End Property
'----------------------------------------------------------------------------------------------
Public Property Get FullName() As String
Dim sTemp As String
'first name
sTemp = Trim(Me.FirstName) & " "
'middle name, if any
If Trim(Me.MiddleName) <> "" Then
sTemp = sTemp & Trim(Me.MiddleName) & " "
End If
'last name
sTemp = sTemp & Trim(Me.LastName)
'in case we have empty names
sTemp = Trim(sTemp)
FullName = sTemp
End Property

(I'm not totally crazy about this vba parser, as it doesn't make my code look like my code, but I do love the pretty colors)

And what I utilize is more along the lines of the following:
FirmPerson class

Option Explicit

Public FirstName As String
Public LastName As String
Public MiddleName As String
'----------------------------------------------------------------------------------------------
Public Property Get FullName() As String
Dim sTemp As String
'first name
sTemp = Trim(Me.FirstName) & " "
'middle name, if any
If Trim(Me.MiddleName) <> "" Then
sTemp = sTemp & Trim(Me.MiddleName) & " "
End If
'last name
sTemp = sTemp & Trim(Me.LastName)
'in case we have empty names
sTemp = Trim(sTemp)
FullName = sTemp
End Property

Since most of the stuff is just a read/write property with no validation, I can get away with just having public variables. Although the derived read-only property (FullName), obviously requires coding.

Maybe this is a "classes for dummies" type post. But I've had this conversation with a couple of other good programmers recently, and I think I've helped them with the way I think about classes.

Using the simplified FirmPerson Class, this is then a simplified version of my FirmInfo class (I'll hard code a couple of people, since how you get the people is a different concept)
FirmInfo class

Option Explicit
Option Base 1

Private m_People As Collection
Private m_aryPeople() As String
'-----------------------------------------------------------------------------------------------
' The people at the firm
'-----------------------------------------------------------------------------------------------
Public Property Get People() As Collection
'check our private collection
If m_People Is Nothing Then
GetSomeFakePeople
End If
Set People = m_People
End Property
'-----------------------------------------------------------------------------------------------
' get some people, quick and dirty like, just for purposes of the demo
'-----------------------------------------------------------------------------------------------
Private Sub GetSomeFakePeople()
Dim oMyPerson As FirmPerson

'create a new instance of our private people collection
Set m_People = New Collection

'get a new instance of a person
Set oMyPerson = New FirmPerson
'set some values
oMyPerson.FirstName = "John"
oMyPerson.MiddleName = "Jacob"
oMyPerson.LastName = "Smithers"
'add to our collection of people
m_People.Add oMyPerson
'release the memory
Set oMyPerson = Nothing

'rinse and repeat
Set oMyPerson = New FirmPerson
'set some values
oMyPerson.FirstName = "Jane"
oMyPerson.MiddleName = "Jinglemeyer"
oMyPerson.LastName = "Smith"
'add to our collection of people
m_People.Add oMyPerson
'release the memory
Set oMyPerson = Nothing

End Sub
'-----------------------------------------------------------------------------------------------
' Pass in a name, see if the name exists
'-----------------------------------------------------------------------------------------------
Public Property Get PersonExists(sFullName As String) As Boolean
Dim i As Integer
Dim bRet As Boolean

On Error GoTo l_err

'go through the index to find a match (and verify using the FullName as a Key
For i = 1 To m_People.count
'if we find a match, then we will be able to reference it
If sFullName = m_People(i).FullName Then
bRet = True
Exit For
End If
Next

l_exit:
PersonExists = bRet
Exit Property
l_err:
bRet = False
Resume l_exit
End Property
'-----------------------------------------------------------------------------------------------
' for easily adding to combo/dropdown userform ontrols using .List property
'-----------------------------------------------------------------------------------------------
Public Property Get Array_PeopleFullNames() As Variant
Dim i As Integer
i = Me.People.count
ReDim Preserve m_aryPeople(i)
For i = 1 To Me.People.count
m_aryPeople(i) = Me.People(i).FullName
Next
Array_PeopleFullNames = m_aryPeople
End Property

And then the last piece is a regular module which has the following (this allows me to access my class all the time, although it may not be appropriate in all cases of class uses, since this is essentially using my class as a global variable, rather than a repeatable organization structure--however, this concept will also be useful for the Ribbon discussion in the next post):

Option Explicit
Public pub_FirmInfo As FirmInfo
'-----------------------------------------------------------------------------------------------
' Publically accessible version of our firm info class
'-----------------------------------------------------------------------------------------------
Public Function myFirmInfo() As FirmInfo
If pub_FirmInfo Is Nothing Then
Set pub_FirmInfo = New FirmInfo
End If
Set myFirmInfo = pub_FirmInfo
End Function

I think it helps to use the watch (both locals and regular) windows when you're playing around with classes.

For me, they are just ways of organizing my own thoughts... so that when I come back to the code later, if I've named my stuff well, I don't really have to read the code to figure out what it's supposed to do.

This was longer than I meant, but I wanted to lay out why I was looking at a class for the Ribbon Controls, so it sort of contextually makes sense. I think.

In answer to one of your other questions: no, it doesn't dynamically load content yet, because this class is still very much a work in progress. I've read enough to know a lot of what I can do in terms of Ribbon stuff... it's more about having some people who have more experience weigh in on design architecture.

If someone who understands what I'm doing says "that's all well and good, but I think you just want to do most of this customization you're doing in the XML, and leave the rest of it to a one-off type thing in vba" then that would be great to know.

At the same time, if I help someone who is struggling to work with the Ribbon by showing my way of organizing my thoughts... that'd be great.

Thanks for reading.

- Jason aka "Frosty"

p.s. Greg: if you (or anyone, really) wants to adapt and change this or any of my code for the purposes of creating another of your excellent tips, please feel free. Creating tips for other people is not something I'm in the habit of, so I'm sure you could create something more simple and coherent than I can.

Frosty
02-19-2011, 08:05 PM
Whoops. Darn it, that's what happens when you remove too much stuff and don't check all the properties.

In my original class, on initialization I actually populate my data sets. So there are two workable solutions, although what you're doing works too for our purposes.

1. In the PersonExists property, refer to me.People instead of m_People in the first instance... that will take care of making sure People is populated.

2. Run the GetFakePeople in the Class_Initialize routine (which is more of the way the "real" class would work... when the class gets initialized, make sure all of your private variables will hold any data they need to).

You don't have to do that, if you use a bunch of Me.WhateverMyPublicProperty references are from within the class... but it's easier to step through the class if you refer to the private variables.

Sorry for the delay in the actual working project... I got about halfway through that process before getting distracted by chores from my wife. Grin.

gmaxey
02-20-2011, 07:23 AM
Ok. Yes either works with the following.

Sub Test()
Dim arrTest() As String
Dim i As Long
MsgBox myFirmInfo.PersonExists("John Jacob Smithers")
arrTest = myFirmInfo.Array_PeopleFullNames
For i = 1 To UBound(arrTest)
Debug.Print arrTest(i)
Next i
End Sub


Thanks for the explanation. Take you time on the full demo. Those of us with a wife fully understand.

Paul_Hossler
02-20-2011, 11:30 AM
Just a different style (no better, no worse, just preference)

I'd have a standard module for the procedural stuff, and a class for a Person and a class for the People (Firm?)

Standard Module:

Option Explicit
Dim oFord As New FirmPeople
Dim oHonda As New FirmPeople

Sub test()

Dim sJohn As String

Call oFord.PersonAdd("John", "Jacob", "Smithers")
Call oFord.PersonAdd("Jane", "Jinglemeyer", "Smith")
Call oFord.PersonAdd("Tom", "", "Jones")

Call oHonda.PersonAdd("Mary", "Astor", "Brown")
Call oHonda.PersonAdd("Tommy", "Lee", "Jackson")


MsgBox "There are " & oFord.HowManyPeople & " people at Ford"
MsgBox "There are " & oHonda.HowManyPeople & " people at Honda"

sJohn = "John Jacob Smithers"

MsgBox "Does " & sJohn & " work at Ford? " & oFord.PersonExists(sJohn)

If oFord.PersonExists(sJohn) Then
oFord.Person(sJohn).DOB = #2/14/2000#
End If

MsgBox sJohn & "'s Date of Birth is " & oFord.Person(sJohn).DOB


Set oFord = Nothing
Set oHonda = Nothing

End Sub


Person class and People class in the attachment

That was the Person object/class captures the attributes and methods about a single person, and the People class captures the perperties and methods for the single Company.

The Collection stuff is very rough, and needs error checking, etc. but it's just something else to consider

Paul

Frosty
02-20-2011, 12:53 PM
Paul, thanks for adding to the discussion.

Couple questions for you:

1. Do you put DOB as an empty get/let pair out of habit or for the argument of having the placeholder in the class in case you want to add data validation later? Or some other reason?

My contention is that the empty let/get structure is generally the most scary thing about utilizing classes for someone new to them, and I was wondering what you felt are the pros/cons of putting in that structure even if it's not utilized.

I've had very established programmers with successful macro packages get out of using classes entirely because of the perceived unnecessary overhead of setting up a private module level variable placeholder every single time they set up a new property to the class, even when that property is read/write with no data validation.

The difference between adding
Public Email as String
Public DOB as Date
Public PreferredClosing as String

as public variables, vs. adding in the let/get property pairs for those three items has seemed to make the "cost of entry" into using classes too high.

2. When you say standard module for the procedural stuff... what do you mean? Putting the data into the class?

I'll have more thoughts when I can upload the macro template, but I'm trying to make sure it works first ;)

Paul_Hossler
02-20-2011, 01:51 PM
<<Opinion Mode = On >>

1. IF I'm using classes ( equivalnet to Objects in OOP) I try to 'map' real life, like the Person class in my example

So a 'Person' has 'Properties' = First, Middle, and Last name, and a DOB (plus a lot of others).

A 'Person' also has 'Methods' or things that they can do, like 'BuyHouse', etc.

OOP fanatics want EVERYTHING to come and go through properties, i.e. nothing exposed from inside the object ('encapsulation'). That way if the object changes internally, the exterior still looks the same. Like all rules, the purpose of that rule is so that you're aware of what you're doing if you break it. (like your EMail, DOB, PreferredClosing examples).

Something like this puts the Trim() into the object when the data goes into it, so that I don't need to Trim() for every time and I know that my data is good


Property Let FirstName(sFirstName As String)
m_sFirstName = Trim(sFirstName) ' added TRIM()
End Property


If oTom, oDick, and oHarry are dimmed as instances of the Person object, then oTom.BuyHouse ( ) and oHarry.DOB = ... make the program more maintainable since all of the 'Person' logic is in one place and (if it's debugged) I don't need to worry

The really big (OK, huge) VBA lack is the ability to assign an object instance to another instance of the same type:

oAnyPersion = oFord.GetPerson ("John")

If you just use Set

Set oAnyPersion = oFord.GetPerson ("John")

You're only setting the pointers to the same object, de-referencing oAnyPerson and the "John" element of the collection are the same item


If oFord.PersonExists(sJohn) Then
oFord.Person(sJohn).DOB = #2/14/2000#
End If

MsgBox sJohn & "'s Date of Birth is " & oFord.Person(sJohn).DOB

Set oSomeOne = oFord.Person(sJohn)
oSomeOne.DOB = #5/7/1948#

MsgBox sJohn & "'s Date of Birth is " & oFord.Person(sJohn).DOB


Now John's DOB has been changed

2. My preference is to have each class represent a defined ( = concrete) entity (like a Person and a Company), with properties and methods appropriate to the entiry, and only those properties and methods

The task of using those objects falls to a standard module or a non-class module

<<Opinion Mode = Off >>

Paul

fumei
02-21-2011, 01:31 PM
"Whoops. Darn it, that's what happens when you remove too much stuff and don't check all the properties."

<smile>

fumei
02-21-2011, 01:33 PM
BTW: I think there may be a few of us who are quite happy Frosty (Jason) has joined us here.

BTW2: I agree with Paul.

fumei
02-21-2011, 02:59 PM
Jason: "So just a quick primer on my typical approach, before I get to the real nuts and bolt in a later post where I actually upload a macro template with the whole ribbon class (since there will be a lot of code to that)."

We are going to hold you to that...please. That is what I want to see. Hopefully before March 17th, at which time I will be going off-line. That is my last day at work (where I have Internet access).

Frosty
02-21-2011, 07:14 PM
Gerry: should be able to accommodate with plenty of time to spare.:beerchug:

Paul: That (essentially) ByRef issue with custom classes is not something I'd ever really thought about. As a non-formally trained programmer (although with some really good mentors at one point), my "entry" into programming was from VBA, so I guess I didn't know what I was missing. I can see where that would be useful functionality.

However, everything else you said I think I already agree with, except for programming something according to the way a "fanatical OOP" programmer would want it, rather than in a way I find much more readable and accessible.

I've made the joke in the past... if I wanted to program in something which was indecipherable, I'd go learn one of the versions of C... but VB/VBA already lends itself to being readable by a non-geek. So the more I do to make my programming readable by a non-geek, the easier it is to have other people benefit. And that's kind of been my approach to classes, although I totally agree with everything you said about the strategy of what should be where.

Enough philosophy... now I'll spend time showing what I mean.

Thanks for all the feedback/input. I really appreciate the willingness to use the <<Opinion Mode>> feature ;)

Paul_Hossler
02-22-2011, 09:09 AM
Some thoughts --

There are advantages to using Property to access object level variables.

Example of 'Person' class module


Option Explicit

'by default these are private to the instance of the class
Dim c_DOB_Year As Long, c_DOB_Month As Long, c_DOB_Day As Long
Dim c_EligableRetire As Date
Dim c_RetirementAge As Long

Property Let InitDOB(N As Long, d As Date)
c_DOB_Year = Year(d)
c_DOB_Month = Month(d)
c_DOB_Day = Day(d)
c_RetirementAge = N

c_EligableRetire = DateSerial(c_DOB_Year + c_RetirementAge, c_DOB_Month + 1, 0)
End Property

Property Get DOB() As Date
DOB = DateSerial(c_DOB_Year, c_DOB_Month, c_DOB_Day)
End Property

Property Let ChangeRetirementAge(N As Long)
c_RetirementAge = N
c_EligableRetire = DateSerial(c_DOB_Year + c_RetirementAge, c_DOB_Month + 1, 0)
End Property

Property Get HowManyMoreDays(Optional FormatPretty As Boolean = False) As Variant
If FormatPretty Then
HowManyMoreDays = Format(c_EligableRetire - Now, "#,##0 days to go")
Else
HowManyMoreDays = CLng(c_EligableRetire - Now)
End If
End Property

Property Get RetirementDate(Optional FormatShort As Boolean = False) As Variant
If FormatShort Then
RetirementDate = Format(c_EligableRetire, "yyyy-mmm")
Else
RetirementDate = c_EligableRetire
End If
End Property




and the Standard module that uses 3 instances of 'Person'


Option Explicit

Sub Paul1()
Dim Tom As New Person
Dim Dick As New Person
Dim Harry As New Person

Tom.InitDOB(65) = #2/14/1960#
Dick.InitDOB(65) = #7/4/1980#
Harry.InitDOB(65) = #12/25/1990#


MsgBox "Tom -- " & Tom.HowManyMoreDays & " until " & Tom.RetirementDate(True)
MsgBox "Dick -- " & Dick.HowManyMoreDays & " until " & Dick.RetirementDate(True)
MsgBox "Harry -- " & Harry.HowManyMoreDays(True) & " until " & Harry.RetirementDate

Harry.ChangeRetirementAge = 70
MsgBox "Harry Again -- " & Harry.HowManyMoreDays(True) & " until " & Harry.RetirementDate

End Sub


I agree with the need to have readable and maintainable code (I can't even spell C++) but I think that by encapsulating the Person object (for ex) with all the properties and methods associated, and then when I use 'Person' like in the standard module above, I find that it makes it easier to read and follow the program flow

Paul

Frosty
02-22-2011, 11:18 AM
Paul,

It's "See plus plus"

Grin.

Thanks for the concrete examples. I think you and I agree 100% on the philosophy behind when to use a class and how it should look outside the class (i.e., which dirty laundry to hide).

However, I think the point I might be failing to make is the differences between using a Public Type, a simplified version of a class, and the more robust version of a class.

This...

Public Type Person
FirstName as String
MiddleName as String
LastName as String
DOB as Date
End Type
versus this... a class module named Person, with the following contents

Option Explicit
Public FirstName as String
Public MiddleName as String
Public LastName as String
Public DOB as Date
versus this, a class module named Person, with the following contents

Option Explicit

Private m_FirstName As String
Private m_MiddleName As String
Private m_LastName As String
Private m_DOB As Date

Public Property Let FirstName(sFirstName As String)
m_FirstName = sFirstName
End Property
Public Property Get FirstName() As String
FirstName = m_FirstName
End Property
Public Property Let MiddleName(sMiddleName As String)
m_MiddleName = sMiddleName
End Property
Public Property Get MiddleName() As String
MiddleName = m_MiddleName
End Property
Public Property Let LastName(sLastName As String)
m_LastName = sLastName
End Property
Public Property Get LastName() As String
LastName = m_LastName
End Property
Public Property Let DOB(dtDOB As Date)
m_DOB = dtDOB
End Property
Public Property Get DOB() As Date
DOB = m_DOB
End Property

Obviously there are major limitations in using a Public Type versus a Class, although depending on the usage... the loss of functionality from a class to a public type may not be all that much for the programmer. And you do get the major benefit of being able to organize data in a particular way that makes the code using this structure more readable.

In all of the above examples you can set your Person somehow and utilize myPerson.FirstName = "Jason"

My contention is that, as written, the two above classes function in exactly the same way. And yet you always seem to see the second way demonstrated (which is what I mean by "empty let/get pairs"), and thus very smart and good programmers deem using the class an inordinate amount of work for the limited gain.

I guess I'm just advocating choosing Door #2 whenever possible (for multiple reasons: a more readable class, easier to step through the code, watch window and locals window are a lot more friendly, etc)... but I would still advocate stuffing the dirty laundry behind Door #3 when #2 isn't possible (i.e., whenever you need to do internal calculations on data immediately available to the class).

Will have a concrete example shortly.

Thanks again for discussing.

- Jason

Paul_Hossler
02-22-2011, 06:21 PM
Jason -- We're probably in pretty loud agreement at least

You're correct about the 'No Added Value' Let/Get pairs not adding much if all they do is assign and retreive intra-class variables.

IMHO a class that is mostly Public variables (your #2) could more easily done other ways (e.g. your #1 and arrays or collections), BUT using a Class still works and can be usefully used.

Paul

Frosty
02-23-2011, 09:55 AM
Apologies in advance for the long post, trying to be comprehensive about where I'm coming from and where this is at right now. I could tweak this forever (like all code), but I wanted to get the concept out and see if I'm barking up the wrong tree, as it were.

1. Applications I've used so far
Word 2010 (although I believe most of this stuff would be applicable to Word 2007 as well)
Gimp (GNU Image Manipulation Program) www.gimp.org (http://www.gimp.org) -- a surprising full feature graphic manipulation program
Custom UI Editor for Microsoft Office (has some bugs, but indispensable for working with the ribbon)
2. Goals:
a) a more robust way of working with the ribbon without constantly having to close my project (and/or my application) in order to update my user
interface.
b) an organized and flexible way of adding/adjusting and adapting the interface, although targeted towards developers (the MS interface in 2010 is pretty good for non-developers, it seems)
c) maybe encouraging Microsoft to provide this functionality natively, rather than having various developers cobble together work-arounds. In short-- to encourage MS to continue robust support for real development in the VBA environment.
3. What this demo ... ummm... demonstrates (as this list grows, it occurs to me I'm just trying to list out the various things it does-- I've simplified as much as I can, but these are fairly core components of any word project I develop)
a) how to use dynamic custom images in a project, without having to "hard code" any values within the XML (i.e, using getImage with custom images)
b) how to always have access to the ribbon object, without having to reload the project
c) my current organizational concept for a class called "RibbonControls" where the class contains the way to access all of my UI customizations. Admittedly, much of this information is static for the duration of the word session. So it could easily be in the xml, rather than an array in VBA.
d) How I've separated my classes and modules in order to effectively have multiple people work on separate aspects of a word development project (i.e., incremental improvements to the RibbonControls class shouldn't necessarily invalidate the particular customizations to the Ribbon.
e) A custom tab called "UI Examples" where I'm essentially playing with the various controls I might typically use
f) A custom tab called "Word 2003 Legacy" which basically mimics the 2003 layout. I found this code over a year ago, and somehow lost the attribution (I blame the xml editor, not my own lack of organization *grin*). I believe it was from something in the mvps website, but I don't remember. My apologies if you happen to read this and discover it is yours
g) how to utilize some application events
h) my standard error trapping
i) some other various misc utilities I've found useful in every project I've ever developed (conversion routines, etc)
j) last minute addition of greg maxey's dynamicMenu stuff (with some slight reformatting), because it will be a good "capabilities" demonstration when I'm working with a trainer on the development project which engendered this whole investigation/
4. Current concerns.
a) not sure if I should separate out RibbonControls and have a separate class RibbonControl. Both concepts are sort of smooshed together at the moment.
b) The RibbonControls class has some structural issues which I need to address (more private variables, I suspect).
c) Haven't gotten the combobox dropdown to clear out after being used... not sure why.
This is very much a work in progress (and an under-developed concept). Any and all comments welcome and appreciated. Any use of this code is absolutely allowed by me, without the need to give me credit (if you find it useful). I don't care about credit, but some people do, so wherever I've attributed someone else, please retain that acknowledgment if you find the code useful.

I do not currently have a real mechanism for some of the more exotic controls, mostly because I just haven't gotten to them yet.

I realize a lot of what I've sketched out here is, in actuality, a different place to store the "static" information about the user-interface, rather than dynamic information. So I may have missed the forest for the trees here, somewhat.

At the same time, as I'm testing various functions, I appreciate having programmatic access to my entire user-interface... rather than having to exit out into XML in order to correct a spelling mistake in a super-tip.

I guess I'm hoping those of you with more experience will be able to weigh in on where it looks like I'm wasting development effort.

So this is definitely not a finished product, but rather representative of where I'm at in learning the new way of interacting with the Word user interface. This is a huge change from the CommandBars structure (which had lots of problems, but I basically knew how to avoid them all... and if I came up against the unsolvable corruption... all I needed was a commandbar with my icons on it, and I could programmatically rebuild my entire user interface from scratch).

Just a little bit of background on why I've done what I've done here.

Not sure there's anything really new in here, other than a consolidation of stuff which has been better articulated elsewhere. I dunno, maybe I'm suddenly getting shy. Grin.


- Jason

p.s. curious that you cannot upload a .dotm. However, a .dotm is required in this case (I think), so I've uploaded a .zip file version (which I think will need to be uncompressed, since I did not just change the file-extension)

gmaxey
02-23-2011, 05:15 PM
Jason,

A lot of stuff to look at here. Thanks for posting it. It will take me a while to find time to give this the attention it deserves. I will do that though.

As for 4.c. To clear the combobox after the change event you have to invalidate the control as you have done and then use the comboBox getText dynamic attribute to set the value to ""

Sub GetText(control As IRibbonControl, ByRef returnedVal)
returnedVal = ""
End Sub

Shred Dude
03-12-2011, 08:23 PM
Frosty:

Thanks for posting your approach. I'm just starting to find some time to wade through it.

First glance brought up a nagging question I have that I haven't been able to test so far as I only have OFfice 2007. If I create a file in 2007 with a custom UI, and send it to someone running Office 2010, will that ribbon function for the 2010 user? Or do I need to provide two XML segments, with different schemas saved in the same file? Or better yet adapt an approach similar to yours here?

Your sections with the conditional compilation triggered this question for me.

BTW, slight Typo (I think) at the Top of your Ribbon Module. The constant "RIBBON_SCHEME" is not used anywhere else in the project.

'need these two constants in order to have xml dynamic content
#If VBA7 Then
Private Const RIBBON_SCHEMA As String = "http://schemas.microsoft.com/office/2009/07/customui"
#Else
Private Const RIBBON_SCHEME As String = "http://schemas.microsoft.com/office/2006/01/customui"
#End If

gmaxey
03-13-2011, 06:49 AM
Keith,

Any ribbons that I have created using the 2007 version of the schema have worked without issue with Word 2010. Jason's (aka Frosty) Ribbon_Schema constant is used in the Public Sub GetContent procedure of the Ribbon module.

Shred Dude
03-13-2011, 04:11 PM
Greg:

Thanks for the reassurance on 2007 ribbons working in 2010. I was hoping that was the case.


The variable that I was pointing out to be unused elsewhere in the presented code is "RIBBON_SCHEME"... not "RIBBON_SCHEMA".

I haven't tried to run the code but it would seem to me that the code as presented when run on Office 2007 (not VBA 7) would insert a blank string variable for the Schema in the generated XML code, as the getContent() routine that generates the XML only refers to "RIBBON_SCHEMA", and never to "RIBBON_SCHEME".

"RIBBON_SCHEME" is only referenced the one time in the whole project when it is set if not running on VBA 7. So, if you fired this up on Office 2007, wouldn't the "RIBBON_SCHEMA" variable referred to in getContent be an empty string? Or worse yet, would the getcontent() routine crash because it refers to a constant that was never compiled?

Again, just reading the code, haven't tried it. I'm not familiar with the effect of the conditional compilation statements (#IF...), so maybe I'm off base here.

Thanks again for the feedback.

Frosty
03-13-2011, 04:23 PM
Thanks for looking at the code, Shred... that's definitely a typo. I haven't played with the code in 2007, so I didn't notice the error yet, but they should be the same.

I've been a little offline as I've got a baby coming very soon... but I hope to keep this thread alive as various people weigh in with how they do UI customization from the developer point of view (2010 makes the UI customization on an end-user basis way easier).

gmaxey
03-13-2011, 04:42 PM
Keith,

Since the declaration is conditional when the GetContent procedure fires using Word 207 a Compile Error - Variable not defined occurs with the typo "Ribbon_Scheme" uncorrected. So yes, the GetContent procedure will crach due to the typo.



Greg:

Thanks for the reassurance on 2007 ribbons working in 2010. I was hoping that was the case.


The variable that I was pointing out to be unused elsewhere in the presented code is "RIBBON_SCHEME"... not "RIBBON_SCHEMA".

I haven't tried to run the code but it would seem to me that the code as presented when run on Office 2007 (not VBA 7) would insert a blank string variable for the Schema in the generated XML code, as the getContent() routine that generates the XML only refers to "RIBBON_SCHEMA", and never to "RIBBON_SCHEME".

"RIBBON_SCHEME" is only referenced the one time in the whole project when it is set if not running on VBA 7. So, if you fired this up on Office 2007, wouldn't the "RIBBON_SCHEMA" variable referred to in getContent be an empty string? Or worse yet, would the getcontent() routine crash because it refers to a constant that was never compiled?

Again, just reading the code, haven't tried it. I'm not familiar with the effect of the conditional compilation statements (#IF...), so maybe I'm off base here.

Thanks again for the feedback.

gmaxey
03-13-2011, 04:44 PM
Jason,

I have been offline for a bit as well so I haven't had any time to continue with the table method I passed to you a week or so ago. Have you had a chance to look at it closely yet?

As soon as I have time I want to try to replicate what you have done using the array and then I will post the demonstration document to this thread.



Thanks for looking at the code, Shred... that's definitely a typo. I haven't played with the code in 2007, so I didn't notice the error yet, but they should be the same.

I've been a little offline as I've got a baby coming very soon... but I hope to keep this thread alive as various people weigh in with how they do UI customization from the developer point of view (2010 makes the UI customization on an end-user basis way easier).

Frosty
03-13-2011, 06:11 PM
Haven't looked at it closely enough, yet... but I agree that table or array approach (and I was actually thinking of table > array > class)... you'll still end up having to customize something somewhere other than in vba.

However, as you know, it actually takes time to focus on this... and I haven't had that.

Paul_Hossler
03-14-2011, 02:14 PM
I believe that you only need the 2010 schema in the xml if you're using the functionality new to 2010, like Backstage

I've used the 2007 code and XML in 2010 (forwards compatible).

Paul