在過去的兩個星期裏, 我一直在做我的第一個真正的.net WinForm應用的開發. 這是一個很有趣的過程,我一直在瘋了似的學習東西. 其中之一就是我要允許應用程序能夠用微軟的Application Updater Block進行自我更新。 當它正常工作的那一刻,讓我有一種很大的成就感,同時我也意識到微軟沒有提供那種按步驟順序的例子。 Duncan Mackenzie 有一個 很好的blog文章 可以做一個開始,但是這個例子是VB做的並且沒有提供RSA公鑰和私鑰的細節情況,所以我決定說一下我的工作過程。 希望能對你有用!
Step #1 Install the Application Blocks
Download the Updater Application Block from Microsoft .
Run the MSI Installer.
Step #2 在項目中加入代碼和引用:
把下列工程加入到你的WinForm工程所在的解決方案:
Microsoft.ApplicationBlocks.ApplicationUpdater
Microsoft.ApplicationBlocks.ApplicationUpdater.Interfaces
Microsoft.ApplicationBlocks.ExceptionManagement
Microsoft.ApplicationBlocks.ExceptionManagement.Interfaces
如果你選擇默認安裝的話,它們的位置可能是:
C:/Program Files/Microsoft Application Blocks for .NET/Updater/Code/CS/Microsoft.ApplicationBlocks.Updater
在你的WinForm工程中引用下列工程
Microsoft.ApplicationBlocks.ApplicationUpdater
Microsoft.ApplicationBlocks.ApplicationUpdater.Interfaces
Microsoft.ApplicationBlocks.ExceptionManagement
把下列命名空間加入到你Form的.cs文件中
using System.Runtime.Serialization;
using System.Threading;
using System.Diagnostics;
using System.IO;
using System.Xml;
然後 添加這個位置的應用程序更新代碼到你的代碼中. 你需要從你的MainForm初始化方法中調用 InitializeAutoUpdate()。
Step #3 生成你應用程序的發佈目錄結構並配置 AppStart.exe
生成一個用於客戶端程序安裝的目錄. 本例子中,我們用如下的目錄:
C:/Program Files/YourApp/1.0.0.0/
現在複製 AppStart.exe 和 AppStart.exe.config 到類似如下的根目錄中
C:/Program Files/YourApp/AppStart.exe
C:/Program Files/YourApp/AppStart.exe.config
說明: 這兩個文件你可以在如下目錄中找到 “C:/Program Files/Microsoft Application Blocks for .NET/Updater/Code/CS/Microsoft.ApplicationBlocks.Updater/AppStart/bin/Debug“
Step #4 修改 AppStart.exe.config 文件
AppStart.exe 會啓動你的應用程序,如果更新文件下載完成之後還有可能要重啓. 它需要知道啓動你最新的程序的目錄位置.
修改配置文件以配合當前的版本:
<ClientApplicationInfo>
<appFolderName>C:Program FilesYourApp1.0.0.0</appFolderName>
<appExeName>YourAppName.exe</appExeName>
<installedVersion>1.0.0.0</installedVersion>
<lastUpdated>2004-06-10T15:33:17.3745836-04:00</lastUpdated>
</ClientApplicationInfo>
</appStart>
Step #5: 生成你的公鑰和私鑰
運行 "C:/Program Files/Microsoft Application Blocks for .NET/Updater/Code/CS/Microsoft.ApplicationBlocks.Updater/ManifestUtility/bin/Debug/ManifestUtility.exe"
選擇 “File..Generate Keys” 會提示你是否需要保存: PublicKey.xml 和 PrivateKey.xml 這兩個密鑰接下來就會用到.
我這裏要提醒大家,這些密鑰只要生成一次就可以了, 因爲下面幾個地方需要引用到RSA公鑰和私鑰. 你需要把這些密鑰存放在一個安全的地方,因爲在發佈一個新的更新的時候會用到它
Step #6 創建IIS 虛擬目錄
在你的Web服務器上生成一個目錄來存放你的更新文件. 在這兩個目錄中要放兩樣東西 1) ServerManifest.xml 文件,包含最後版本的一些信息;2) 你的新程序的目錄. 在這個目錄裏,生成一個目錄來存放你的新版本程序. 在我們的例子中,我們用這兩個目錄, C:/Inetpub/AppUpdates 和C:/Inetpub/AppUpdates/1.0.0.1
用 IIS 管理器生成一個虛擬目錄指向剛纔的實際目錄. 記下你的 URL, 在上傳步驟中我們需要用到它. 你必須要打開虛擬目錄的“目錄瀏覽”選項.
Step #7. 配置你的版本 1.0.0.0 的App.config 文件
這裏,我們會需要往裏添加一些新東西. 首先, 我們需要加入一個configSections 元素來定義我們的 appUpdater 節:
<configSections>
<section name="appUpdater" type="Microsoft.ApplicationBlocks.ApplicationUpdater.UpdaterSectionHandler,Microsoft.ApplicationBlocks.ApplicationUpdater" />
</configSections>
接下來,我們需要添加一個 Version 鍵到我們的 appsettings 中, 我們首先設置我們的本地版本爲 1.0.0.0, 這樣我們就可以測試自動更新到版本 1.0.0.1
<appSettings>
<add key="VERSION" value="1.0.0.0" />
</appSettings>
最後,, 加入 appUpdater 節到你的配置文件中. 我這裏用一對方括號把你要修改的值包含起來. 你可以直接從你上一步生成的 PublicKey.xml文件中複製 <RSAKeyValue> 元素.
<xmlFile> 元素必須要指向你在Step #6創建的虛擬目錄的 URL .
<UpdaterConfiguration>
<polling type="Seconds" value="120" />
<logListener logPath="C:Program FilesYourAppUpdaterLog.txt" />
<downloader type="Microsoft.ApplicationBlocks.ApplicationUpdater.Downloaders.BITSDownloader"
assembly="Microsoft.ApplicationBlocks.ApplicationUpdater,Version=1.0.0.0,Culture=neutral,PublicKeyToken=null"/>
<validator type="Microsoft.ApplicationBlocks.ApplicationUpdater.Validators.RSAValidator" assembly="Microsoft.ApplicationBlocks.ApplicationUpdater,Version=1.0.0.0,Culture=neutral,PublicKeyToken=null">
<key>
<RSAKeyValue>
<Modulus>[YOUR MODULUS KEY]</Modulus>
<Exponent>[YOUR EXPONENET]</Exponent>
</RSAKeyValue>
</key>
</validator>
<application name="[YOUR APP NAME]" useValidation="true">
<client>
<baseDir>C:Program FilesYourApp</baseDir>
<xmlFile>C:Program FilesYourAppAppStart.exe.config</xmlFile>
<tempDir>C:Program FilesYourApp emp</tempDir>
</client>
<server>
<xmlFile>http://[YOUR URL]/ServerManifest.xml</xmlFile>
<xmlFileDest>C:Program FilesYourAppServerManifest.xml</xmlFileDest>
<maxWaitXmlFile>60000</maxWaitXmlFile>
</server>
</application>
</UpdaterConfiguration>
</appUpdater>
Step #8 發佈版本 1.0.0.0
設置應用程序版本號. 可以通過設置在 AssemblyInfo.cs 文件中的版本屬性來設置版本號.
[assembly: AssemblyVersion("1.0.0.0")]
編譯應用程序並複製 1.0.0.0 版程序到你程序的 1.0.0.0 目錄中. “C:/Program Files/YourApp/1.0.0.0“
這裏,你需要運行一下 AppStart.exe. 更新過程會失敗,因爲我們並沒有把發佈 ServerManifest XML 文件來指示應用程序新版本是否可用. 你可以檢查日誌文件,位置在 C:/Program Files/YourApp/ 目錄中.
Step #9 構建版本 1.0.0.1
這是最有趣的部分. 首先, 通過更新應用程序的 AssemblyInfo.cs 和 App.config 文件內容來生成修訂版本 1.0.0.1 . 編譯程序, 然後複製文件到step #6生成的Web服務器目錄中.
Step #10 生成服務器的清單文件
這個是最後一步. 如果你對本步驟中的.config文件作了任何修改的話,都必須把本步驟重來一遍. 做法如下:
再次運行 ManifestUtility 程序.
在 “Update files folder“ 選擇器中選擇 1.0.0.1 目錄 .
輸入更新位置的 URL .
輸入新版本號 1.0.0.1
打開之前生成的 PrivateKey.xml 文件.
選擇驗證類 “Microsoft.ApplicationBlocks.ApplicationUpdater.Validators.RSAValidator”
鼠標點擊 CreateManifest, 並保存 ServerManifest.xml 文件到你的虛擬服務器目錄中.
就這些! Pheeew! 從你的 C:/Program Files/YourApp/ 目錄中運行你的 AppStart.exe . 你的程序就會被裝入, 當你的程序運行的時候,你就會得到一個提示 “新版本可用” . 新版本會下載到目錄 C:/Program Files/YourApp/1.0.0.1 中, 然後程序會自動重啓. 如果有任何問題, 記得檢查一下日誌文件. 這些日誌在診斷問題的時候會很有用的.
-Brendan
posted on Thursday, June 10, 2004 11:25 AM
附錄:文中步驟 #2 包含的代碼如下:
/// This handler gets fired by the Windows UI thread that is the main STA thread for THIS FORM. It takes the same
/// arguments as the event handler below it--sender, e--and acts on them using the main thread NOT the eventing thread
/// </summary>
/// <param name="sender">marshalled reference to the original event's sender argument</param>
/// <param name="e">marshalled reference to the original event's args</param>
private void OnUpdaterUpdateAvailableHandler( object sender, UpdaterActionEventArgs e )
{
Debug.WriteLine("Thread: " + Thread.CurrentThread.GetHashCode().ToString());
string message = String.Format(
"Update available: The new version on the server is {0} and current version is {1} would you like to upgrade?",
e.ServerInformation.AvailableVersion,
System.Configuration.ConfigurationSettings.AppSettings["version"] ) ;
// for update available we actually WANT to block the downloading thread so we can refuse an update
// and reset until next polling cycle;
// NOTE that we don't block the thread _in the UI_, we have it blocked at the marshalling dispatcher "OnUpdaterUpdateAvailable"
DialogResult dialog = MessageBox.Show( message, "Update Available", MessageBoxButtons.YesNo );
if( DialogResult.No == dialog )
{
// if no, stop the updater for this app
_updater.StopUpdater( e.ApplicationName );
Debug.WriteLine("Update Cancelled.");
}
else
{
Debug.WriteLine("Update in progress.");
}
}
/**//// <summary>
/// Event handler for Updater event. This event is fired by the originating thread from "inside" the Updater. While it is
/// possible for this same thread to act on our UI, it is NOT a good thing to do--UI is not threadsafe.
/// Therefore here we marshal from the Eventing thread (belongs to Updater) to our window thread using the synchronous Invoke
/// mechanism.
/// </summary>
/// <param name="sender">event sender in this case ApplicationUpdaterManager</param>
/// <param name="e">the UpdaterActionEventArgs packaged by Updater, which gives us access to update information</param>
private void OnUpdaterUpdateAvailable( object sender, UpdaterActionEventArgs e )
{
// using the synchronous "Invoke". This marshals from the eventing thread--which comes from the Updater and should not
// be allowed to enter and "touch" the UI's window thread
// so we use Invoke which allows us to block the Updater thread at will while only allowing window thread to update UI
this.Invoke(
new MarshalEventDelegate( this.OnUpdaterUpdateAvailableHandler ),
new object[] { sender, e } );
}
/**//// <summary>
/// This handler gets fired by the Windows UI thread that is the main STA thread for THIS FORM. It takes the same
/// arguments as the event handler below it--sender, e--and acts on them using the main thread NOT the eventing thread
/// </summary>
/// <param name="sender">marshalled reference to the original event's sender argument</param>
/// <param name="e">marshalled reference to the original event's args</param>
private void OnUpdaterDownloadCompletedHandler( object sender, UpdaterActionEventArgs e )
{
Debug.WriteLine("Download Completed.");
}
/**//// <summary>
/// Event handler for Updater event. This event is fired by the originating thread from "inside" the Updater. While it is
/// possible for this same thread to act on our UI, it is NOT a good thing to do--UI is not threadsafe.
/// Therefore here we marshal from the Eventing thread (belongs to Updater) to our window thread using the synchronous Invoke
/// mechanism.
/// </summary>
/// <param name="sender">event sender in this case ApplicationUpdaterManager</param>
/// <param name="e">the UpdaterActionEventArgs packaged by Updater, which gives us access to update information</param>
private void OnUpdaterDownloadCompleted( object sender, UpdaterActionEventArgs e )
{
// using the synchronous "Invoke". This marshals from the eventing thread--which comes from the Updater and should not
// be allowed to enter and "touch" the UI's window thread
// so we use Invoke which allows us to block the Updater thread at will while only allowing window thread to update UI
this.Invoke(
new MarshalEventDelegate( this.OnUpdaterDownloadCompletedHandler ),
new object[] { sender, e } );
}
private void StartNewVersion( ServerApplicationInfo server )
{
XmlDocument doc = new XmlDocument();
// load config file to get base dir
doc.Load( AppDomain.CurrentDomain.SetupInformation.ConfigurationFile );
// get the base dir
string baseDir = doc.SelectSingleNode("configuration/appUpdater/UpdaterConfiguration/application/client/baseDir").InnerText;
string newDir = Path.Combine( baseDir, "AppStart.exe" );
ProcessStartInfo process = new ProcessStartInfo( newDir );
process.WorkingDirectory = Path.Combine( newDir , server.AvailableVersion );
// launch new version (actually, launch AppStart.exe which HAS pointer to new version )
Process.Start( process );
// tell updater to stop
CurrentDomain_ProcessExit( null, null );
// leave this app
Environment.Exit( 0 );
}