|
Microsoft Office Application Development
|
|
Updating An add-in Through the InternetIntroductionWhen you're offering an add-in through your website you usually have no connection to the people who are using your programming work. It is not unusual that you have discovered (or been pointed at) bugs in your code or that you've done some updating to the utility you'd like your users to benefit from. Sometimes it might be useful if your users could be notified automatically of such updates (many software titles have such a mechanism built-in). This little article shows a way to include that functionality with your add-in. Assumptions for this articleI've assumed the following:
Excel 2007 peculiaritiesThe web query is inserted using VBA, because Excel 2007 disables all connections in an addin by default, until the user clicks the button on the message bar (top of Excel window, below the ribbon) and enables them:
Also, it appears Excel 2007 does not allow inserting a webquery in one of the add-in's worksheets. To work around this problem, the add-in's "IsAddin" property is temporarily set to False, then the web query is inserted and the property is set back to True. Updating mechanismThe updating process works as follows:
Update modesThe code handles two modes, automatic updates and manual update checks. In the automatic case, the webquery is added to the addin, but is refreshed asynchronously. A worksheet change event is scheduled in the class module, which will fire as soon as the webquery has finished updating (or has timed out). This is done so the check for an update can run in the background whilst Excel finishes its startup process. The user won't have to wait (well, not noticably anyway) for your update to do its work. In the manual case, the web query is updated synchronously (nothing will work in Excel until it has retrieved its result). I did this because in this case you will confuse your user of he does not get "immediate" feedback on the result. CodeThe code that does the actual updating is wrapped in a class module called "clsUpdate", see the entire code below. '-------------------------------------------------------------------------' Module : clsUpdate ' Company : JKP Application Development Services (c) ' Author : Jan Karel Pieterse ' Created : 19-2-2007 ' Purpose : Class to check for program updates '------------------------------------------------------------------------- Option Explicit Public WithEvents Sht As Worksheet Private Declare Function URLDownloadToFile Lib "urlmon" _ Alias "URLDownloadToFileA" (ByVal pCaller As Long, _ ByVal szURL As String, ByVal szFileName As String, _ ByVal dwReserved As Long, ByVal lpfnCB As Long) As Long Private mdtLastUpdate As Date Private msAppName As String Private msBuild As String Private msCheckURL As String Private msCurrentAddinName As String Private msDownloadName As String Private msTempAddInName As String Private mbManual As Boolean Private Sub Class_Terminate() Set Sht = Nothing End Sub Private Sub DownloadFile(strWebFilename As String, strSaveFileName As String) ' Download the file. URLDownloadToFile 0, strWebFilename, strSaveFileName, 0, 0 End Sub Public Function IsThereAnUpdate(Optional bShowMsg As Boolean = False) As Boolean Dim sNewBuild As String On Error Resume Next SaveSetting AppName, "Updates", "LastUpdate", CStr(Int(Now)) If Err.Number <> 0 And bShowMsg Then MsgBox "Error retrieving update information, please try again later.", vbInformation + vbOKOnly End If End Function Public Sub DoUpdate() Dim sNewBuild As String sNewBuild = ThisWorkbook.Names("Available_build").RefersToRange.Value If Len(sNewBuild) = 0 Or Len(sNewBuild) > 4 Then MsgBox "Unable to fetch version information, please try again later.", vbOKOnly + vbInformation Exit Sub End If If CLng(sNewBuild) > CLng(msBuild) Then If MsgBox("We have an update, do you wish to download?", vbQuestion + vbYesNo) = vbYes Then DownloadName = "http://www.jkp-ads.com/downloadscript.asp?filename=" & ThisWorkbook.Name If GetUpdate Then Application.Cursor = xlDefault MsgBox "Successfully updated the addin, please restart Excel to start using the new version!", vbOKOnly + vbInformation Else Application.Cursor = xlDefault MsgBox "Updating has failed.", vbInformation + vbOKOnly End If Else Application.Cursor = xlDefault End If ElseIf Manual Then Application.Cursor = xlDefault MsgBox "Your program is up to date.", vbInformation + vbOKOnly End If TidyUp: On Error GoTo 0 Exit Sub End Sub Private Sub Sht_Change(ByVal Target As Range) Application.Cursor = xlDefault If Len(Target.Value) <= 4 Then DoUpdate Application.Cursor = xlDefault ElseIf Manual Then 'Query failed to refresh and was called manually Application.Cursor = xlDefault MsgBox "Unable to retrieve version information, please try again later", vbInformation + vbOKOnly End If Set Sht = Nothing TidyUp: On Error GoTo 0 Exit Sub End Sub Public Sub PlaceBuildQT(ByVal bManual As Boolean) Dim oNm As Name On Error GoTo LocErr Application.ScreenUpdating = False For Each oNm In ThisWorkbook.Worksheets("Sheet1").Names oNm.Delete Next If CInt(Left(Application.Version, 2)) > 11 Then ' Trick!! Somehow Excel 2007 cannot insert a web query into an add-in!!. ThisWorkbook.IsAddin = False End If With ThisWorkbook.Worksheets("Sheet1").QueryTables.Add(Connection:= _ "URL;" & CheckURL, Destination:=ThisWorkbook.Names( _ "Available_Build").RefersToRange) .Name = "autosafebuild" .FieldNames = True .RowNumbers = False .FillAdjacentFormulas = False .PreserveFormatting = True .RefreshOnFileOpen = False .BackgroundQuery = Not bManual .RefreshStyle = xlOverwriteCells .SavePassword = False .SaveData = True .AdjustColumnWidth = False .RefreshPeriod = 0 .WebSelectionType = xlEntirePage .WebFormatting = xlWebFormattingNone .WebPreFormattedTextToColumns = True .WebConsecutiveDelimitersAsOne = True .WebSingleBlockTextImport = False .WebDisableDateRecognition = False ' .WebDisableRedirections = False On Error Resume Next .Refresh BackgroundQuery:=Not (bManual) On Error GoTo 0 If Not bManual Then Set Sht = ThisWorkbook.Worksheets("Sheet1") Else DoUpdate End If End With TidyUp: If CInt(Left(Application.Version, 2)) > 11 Then ThisWorkbook.IsAddin = True ' Trick!! Otherwise, Excel 2007 will ask to save your add-in when it closes. ThisWorkbook.Saved = True End If Application.ScreenUpdating = True On Error GoTo 0 Exit Sub LocErr: If CInt(Left(Application.Version, 2)) > 11 Then ThisWorkbook.IsAddin = True ThisWorkbook.Saved = True End If Application.ScreenUpdating = True Application.Cursor = xlDefault If Err.Description Like "*QueryTables*" Then MsgBox "Error retrieving version information, please try again later.", vbInformation + vbOKOnly Resume TidyUp End If End Sub Public Property Get Build() As String Build = msBuild End Property Public Property Let Build(ByVal sBuild As String) msBuild = sBuild End Property Public Sub RemoveOldCopy() CurrentAddinName = ThisWorkbook.FullName TempAddInName = CurrentAddinName & "(OldVersion)" On Error Resume Next Kill TempAddInName End Sub Public Function GetUpdate() As Boolean On Error Resume Next 'If workbook has been saved readonly, we can safely delete the file! If ThisWorkbook.ReadOnly Then Err.Clear Kill CurrentAddinName End If LastUpdate = Now ThisWorkbook.SaveAs TempAddInName DoEvents Kill CurrentAddinName On Error GoTo 0 DownloadFile DownloadName, CurrentAddinName If Err = 0 Then GetUpdate = True End Function Private Property Get CurrentAddinName() As String CurrentAddinName = msCurrentAddinName End Property Private Property Let CurrentAddinName(ByVal sCurrentAddinName As String) msCurrentAddinName = sCurrentAddinName End Property Private Property Get TempAddInName() As String TempAddInName = msTempAddInName End Property Private Property Let TempAddInName(ByVal sTempAddInName As String) msTempAddInName = sTempAddInName End Property Public Property Get DownloadName() As String DownloadName = msDownloadName End Property Public Property Let DownloadName(ByVal sDownloadName As String) msDownloadName = sDownloadName End Property Public Property Get CheckURL() As String CheckURL = msCheckURL End Property Public Property Let CheckURL(ByVal sCheckURL As String) msCheckURL = sCheckURL End Property Public Property Get LastUpdate() As Date Dim dtNow As Date dtNow = Int(Now) mdtLastUpdate = CDate(GetSetting(AppName, "Updates", "LastUpdate", "0")) If mdtLastUpdate = 0 Then 'Never checked for an update, save today! SaveSetting AppName, "Updates", "LastUpdate", CStr(Int(dtNow)) End If LastUpdate = mdtLastUpdate End Property Public Property Let LastUpdate(ByVal dtLastUpdate As Date) mdtLastUpdate = dtLastUpdate SaveSetting AppName, "Updates", "LastUpdate", CStr(Int(mdtLastUpdate)) End Property Public Property Get AppName() As String AppName = msAppName End Property Public Property Let AppName(ByVal sAppName As String) msAppName = sAppName End Property Public Property Get Manual() As Boolean Manual = mbManual End Property Public Property Let Manual(ByVal bManual As Boolean) mbManual = bManual End Property Alternatively, we could do this with the InternetExplorer control (and thus need a reference to the associated library) . This could be done like this: Public Function IsThereAnUpdateUsingIE() As BooleanDim oIE As InternetExplorer Set oIE = New InternetExplorer With oIE .Navigate2 CheckURL Do Loop Until .Busy = False If Len(.Document.body.innerhtml) > 0 Then If CLng(.Document.body.innerhtml) > CLng(Build) Then IsThereAnUpdate = True End If End If .Quit End With Set oIE = Nothing End Function In a normal module, we create an instance of this class, set it's initial values and do the updating. The comments in the code describe what is being done: Option ExplicitDim mcUpdate As clsUpdate Public Declare Function InternetGetConnectedState _ Lib "wininet.dll" (lpdwFlags As Long, _ ByVal dwReserved As Long) As Boolean Function IsConnected() As Boolean Dim Stat As Long IsConnected = (InternetGetConnectedState(Stat, 0&) <> 0) End Function Sub AutoUpdate() CheckAndUpdate False End Sub Sub ManualUpdate() On Error Resume Next Application.OnTime Now, "CheckAndUpdate" End Sub Public Sub CheckAndUpdate(Optional bManual As Boolean = True) Set mcUpdate = New clsUpdate If bManual Then Application.Cursor = xlWait End If With mcUpdate 'Set intial values of class 'Current build .Build = 0 'Name of this app, probably a global variable, such as GSAPPNAME .AppName = "UpdateAnAddin" 'Get rid of possible old backup copy .RemoveOldCopy 'URL which contains build # of new version .CheckURL = "http://www.jkp-ads.com/downloads/UpdateAnAddinBuild.htm" 'Started check automatically or manually? .Manual = bManual 'Check once a week If (Now - .LastUpdate >= 7) Or bManual Then .PlaceBuildQT bManual End If End With TidyUp: On Error GoTo 0 Exit Sub End Sub Download DemoDownload the demo file here: Update An addin FeedbackSince you have managed to reach the end of this article, maybe you would care to write me a small message, expressing your opinion on this article? Click here to write an email message to me. You may also rate this article: (Rated: 312 times, average rating: 6)![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]()
CommentsShowing last 15 comments of 19 in total (Show All Comments):Comment by: Matthew Pfluger (1/21/2008 11:24:14 AM)Could you implement this for Name manager? It would be helpful. Also, I wrote an add-in that serves as a company-wide storage for custom macros. I placed it on our shared drive and instructed everyone to load the add-in without copying it to their hard drive. I use a WIP version that I overwrite the public version with when I deem it stable. There have been no major problems with this method at this point. Comment by: Jan Karel Pieterse (1/23/2008 11:32:52 PM)Hi Matthew, Good idea. But since NM is also part of a commercial addin by Charles Williams (FastExcel, www.decisionmodels.com) I don't think we can implement that. Comment by: DetePayoday (2/18/2008 1:27:09 PM)The only place where I could find what i was looking for. I will be back soon. Comment by: Brian (2/26/2008 8:21:27 AM)Will this work on Mac machines? I'm guessing "no" since there's the dll call in the "InternetGetConnectedState" function, but it also appears that this function is unnecessary (the "isconnected" variable doesn't get used anywhere, as far as I can tell). I also have concerns about security. I have free, beta and full versions of my app. I would like all of them to have this update capability. Would it be reasonably secure to have each in a separate sub directory on the web, with indexing disabled on each of them (as well as on the next higher directory) so people wouldn't be able to just navigate to the files? Obviously, I would need to put in some #if statements in the code to make sure each version gets the proper update. By the way, thank you so much for what you've made available on this site. It's great! Comment by: Jan karel Pieterse (2/26/2008 9:53:34 AM)Hi Brian, I guess you're correct, IsConnected isn't in use anymore so you could get rid of it. Of course security on your site is a different matter, up to you to decide how you arrange that. I just have the file available in a specific folder. Comment by: Brian (2/26/2008 10:48:03 AM)Will getting rid of IsConnected make it Mac compatible? Comment by: Jan karel Pieterse (2/26/2008 11:24:39 AM)Hi Brain, I hope so, don't have a MAC. Make sure you remove the InternetGetConnectedState declaration lines as well. Comment by: Elashdaf (3/31/2008 7:58:22 PM)Hello Wasjust serfing on net and found this site...want to say thanks. Great site and content! Comment by: jeffersonwhite (4/1/2008 3:14:00 AM)Thanks for an interesting post Comment by: Eric (4/4/2008 1:53:33 PM)Just curious why you have AutoUpdate and ManualUpdate run CheckAndUpdate in different ways, as shown below. Is there some advantage in calling a procedure via "Application.OnTime Now, ..."? I just noticed you can't step through the code if you do it that way - interesting... Sub AutoUpdate() CheckAndUpdate False End Sub Sub ManualUpdate() On Error Resume Next Application.OnTime Now, "CheckAndUpdate" End Sub Comment by: Jan Karel Pieterse (4/6/2008 4:36:29 AM)There is no real reason for the difference here. The Ontime method ensures the entire call tree is finished and gives Excel the chance to finish any pending work. It sometimes prevents unexpected behaviour. Comment by: JPG (4/22/2008 7:08:15 AM)Hey, I'm trying to use a slightly modified version of this and I am having some trouble getting it to work. The problem occurs in RemoveOldCopy(). The first time I update, it works fine, however, any subsequent attempts to update fail because a "Permission denied" error is thrown in RemoveOldCopy(). This is because, after updating once, there is already a file in the XLStart directory named something like Addin.xla(OldVersion) so the line: Kill TempAddInName fails because Excel has the file locked. Even though the file extension has been changed, Excel still loads it. Is there any way to work around this? Comment by: Jan Karel Pieterse (4/22/2008 7:56:17 AM)Hi JPG, The code fails because Excel loads EVERYTHING from the XLSTART folder, whatever it may be called. Addins do not belong in the XLSTART folder, they should be placed anywhere but the XLSTART folder and be "installed" using Tools, Addins, browse. Once setup this way, things should work as advertised. Regards, Jan Karel. Comment by: JPG (4/23/2008 6:52:27 AM)Unfortunately, I don't have a choice, the addin needs to be in the XLStart folder. I worked around the issue by creating a batch file that is run when the addin is closed(when Excel exits) that waits 10s, then deletes the "(OldVersion)" file. Comment by: Jan Karel Pieterse (4/23/2008 8:06:35 AM)Hi JPG, OK, if that is your requirement. You might get away with setting the addin's readonly property to true? Add a comment too!!! | ||||||||||||||||||||||||||||||||||||||
Use the contact page to issue
questions or comments about this website. |