Proactive Logic
Home | About Us | Services | Products | Blog | Contact  | Search
 
 
   
 
 

Archive for the '.Net' Category

Extending Community Server 2007 User Profiles and Vista Setup

Saturday, November 3rd, 2007

Note:  The pdf version of this post has been removed since I cleaned up the code formatting below.

This post will go over my notes on how I accomplished extending the CS schema and code base in order to: acquire additional user fields on user registration, enable modification of these fields in the user control panel, display the new fields in the public view of the user’s profile, and display the new fields for the user in forum posts.  This post should be useful for developers that want to extend their CS schema and also contains content relevant for users just sticking with extended attributes (for example extending a theme with SubForms is useful for both scenarios).

I have just started digging into Community Server 2007 as a pet project of mine. One thing that I wanted to do was link Community Server users with a set of tables that I already have in place. It appears that CS allows users and other objects to be extended by using extended attributes that save as name value pairs to the database. This did not meet my needs so I needed to dig in and extend CS without much documentation on how to do this.  Following this post will require that you are a fairly proficient .Net developer. The examples that I give here assumes that I have users that are linked to a store and they also have a defined affiliation type to that store.  The stores themselves will be filtered by their US state. For example a user can be a Manager of Acme Markets (and Acme markets only exists in 30 states).  I’m actually developing a different community than this, but I changed to code to make it easier to understand for a blog post. This is also the reason I do not include any screen shots, but trust me, it works!

The first thing I did was to install the Community Server 2007 SDK and install the CS schema as per their directions. I am doing this development on a Windows Vista Ultimate based Virtual PC, so I had to do a few things to get CS up and running after downloading the SDK.

My Windows Vista Specific Development Setup

  • Created Virtual directory - classic pipeline .net 2.0 and pointed it at the location of my CommunityServerWeb20 project
  • Updated CommunityServerWeb20 (Internal).csproj in notepad
    • I changed the IISUrl from “http://cs2007″ to point to my virtual directory http://localhost/cs
    • In hindsight, I probably could have just created a host entry for http://cs2007 to point to my virtual directory
  • Followed additional IIS 7 setup instructions for Visual Studio from the following post: http://mvolo.com/blogs/serverside/archive/2006/12/28/Fix-problems-with-Visual-Studio-F5-debugging-of-ASP.NET-applications-on-IIS7-Vista.aspx
  • Disabled ping / keep alive in IIS7 – This setting caused debugger to hang because its purpose is to recycle the ASP.net worker process if it does not respond to a ping, which can happen if you are sitting on a breakpoint for too long.
  • Updated web.config to have debug=”true”
  • Configured the connection strings to point to my CS database

Updating the Data Layer

Once I had the code running I looked at the CS tables and I saw that there were two tables that were candidates for me to extend: cs_Users and cs_UserProfile. As I got further into the code base, I could not see any compelling reason to choose one table over the other since the data access layer seems to always load from both of these tables in order to get the data. I originally extended the cs_Users table, but then I backed out my changes and went with the cs_UserProfile as it just feels more appropriate for the data I was adding.

The User Entity Object

The first thing you should do is add your new properties to the user object which can be found in “\Components\Components\User.cs” file. Theoretically, you probably can stick with extended attributes, and pull off those values later.  I chose to extend the actual user object for my project. 

[Serializable]

public class User : ExtendedAttributes

{
    {Some CS code here}

    /// <summary>
    /// Store that the user is tied to
    /// </summary>
    private int _StoreAffiliatedToId;

    public int StoreAffiliatedToId
    {
        get { return _StoreAffiliatedToId; }
        set
        {
            _StoreAffiliatedToId = value;
        }
    }
 
    /// <summary>
    /// Store affiliation type - Manager, Sales, etc
    /// </summary>
    private int _StoreAffiliationTypeId;
    public int StoreAffiliationTypeId
    {
        get { return _StoreAffiliationTypeId; }
 
       set
        {
            _StoreAffiliationTypeId = value;
        }
    }

Create Update Delete

Searching the CS code base for cs_UserProfile lands you in the “Data Providers\SqlDataProvider\SqlCommonDataProvider.cs” file in the “CreateUpdateDeleteUser” method. It is very straight forward code that is adding parameters to call the “cs_user_CreateUpdateDelete” stored procedure. So back to the database we go to update that stored procedure. I added two new parameters to the “cs_user_CreateUpdateDelete” stored procedure and then updated the Update and Insert statements in that stored procedure to update and insert my new fields into the cs_UserProfile table. Back in the C# code we now need to update the “CreateUpdateDeleteUser” method mentioned above to take properties off of the user object and put them into parameters to call the updated stored procedure.

public override User CreateUpdateDeleteUser(User user, DataProviderAction action, bool createLocalUserOnly, out CreateUserStatus status) {

{some CS code here}

    myCommand.Parameters.Add(“@StoreAffiliatedToId”, SqlDbType.Int).Value = upm.CurrentUser.StoreAffiliatedToId;
    myCommand.Parameters.Add(“@StoreAffiliationTypeId”, SqlDbType.Int).Value = upm.CurrentUser.StoreAffiliationTypeId;

Read

In addition to the “cs_user_CreateUpdateDelete” stored procedure you will need to update the view cs_vw_User_FullUser to retrieve the new values from the cs_UserProfile table to read the new values.

In the C# code you will also need to update the code that takes the fields from the cs_vw_User_FullUser view and puts them into the user object. The code that does that is located in the file “Components\Provider\CommonDataProvider.cs” in the method “cs_PopulateUserFromIDataReader”. It is very simple code that copies values out of a datareader and populates user object properties.

public static User PopulateUserFromIDataReader(IDataReader dr, bool isEditable, bool includeAudit) {

{some CS code here}

     user.StoreAffiliatedToId = (int)dr[“StoreAffiliatedToID”]; 
   user.StoreAffiliationTypeId = (int) dr[“StoreAffiliationTypeID”];

The next step is really optional depending on how you layout your data. My extended cs_UserProfile table contains ID’s that are foreign key references into other tables in my system.  In my case I have an in memory cache that map these ID’s to friendly names. You could add another join to the “cs_vw_User_FullUser” view to get your friendly names from your own data if you so choose. Since I have this data in memory already, I updated the code in the file “Controls\User\UserProfileData.cs” in the method “GetPropertyValue”, to lookup the data for these ID’s from my in memory cache. This “GetPropertyValue” method is called from the Chameleon controls which I will use later in this post to show these new values on the UI.  You’ll see references to “ProactiveLogic.WebFacade” which is an external project that manages my cached data.  My new code is noted by the “new code” comments.



/// <summary>
///
Single value control that provides access to a user’s Profile data and links.

/// </summary>

public class UserProfileData : UserData {

protected override object GetPropertyValue(string property) {
   
   
User user = DataSource as User;
   
   
if (user != null && !string.IsNullOrEmpty(property))
   

 
   
switch (property.ToLower())
   

        // Existing CS code 
       
case “signature”
        // Existing CS code 
       
case “signatureformatted”:
       
{
            
CSContext csContext = CSControlUtility.Instance().GetCurrentCSContext(this.Page); 
             
if (csContext.SiteSettings.AllowUserSignatures && csContext.SiteSettings.EnableUserSignatures && csContext.User.EnableUserSignatures)
                 
return user.Profile.SignatureFormatted.Replace(“\r\n”, “<br />”);
            
else
                 
return string.Empty; 
        
}


        // New code
        case “storeaffiliation”:
           
if (user.StoreAffiliatedToId == 0) 
            
{
                 
return “No Store”;
           
}
           
else
           
{
                 
return ProactiveLogic.WebFacade.CachedData.GetStoreByID(user.StoreAffiliatedToId).Name;
           
}
           
break;
          
            // New code
      
case “storeaffiliationtype”:
           
if (user.StoreAffiliationTypeId == 0) 
            
{
                 
return “No Affiliation”
            
}
           
else
           
{
                 
return ProactiveLogic.WebFacade.CachedData.GetStoreAffiliationTypeById(user.StoreAffiliationTypeId).StoreAffiliationTypeName;
           
}
           
break;


       default:
           
return DataBinder.GetPropertyValue(user.Profile, property); 
       
}


    }
   
else
       
return string.Empty;
  
}


// Existing CS code 
protected
override string GetExtendedAttributeValue(string extendedAttribute)
{
   
throw new InvalidCastException(“CommunityServer.Components.Profile does not support extended attributes.”);
}


   


Now the data layer is done! That is all you have to do. When you see the code it is very simple. Onto the UI…


Updating the Default Theme to Acquire and Edit the additional user profile data


I’m going to cut to the chase and show you the code in that I added to the themes to call my new sub forms to handle this new user data. After that I will show the implementation of the SubForms. I added two new fields one is the store the person is affiliated to, and the second is their affiliation to that store (i.e. Manager, Clerk, etc). My store list needs to be filtered by state, so I wrapped this filtering in an ASP.net Ajax update panel.


Getting the new User Properties on User Registration \ Sign Up


In the file “Web\Themes\default\User\createuser.aspx” I added my two SubForms to the Create User Form CSControl. It is very important that you do not put any spaces in your comma delimited list of SubForms, otherwise they will not be properly processed, as I found out.



<CSControl:CreateUserForm runat=”server” SubFormIds=”StoreAffiliationSubFormID,StoreAffiliationTypeSubFormID” >


I then added call outs to my SubForms, and the ASP.net AJAX Update Panel to make SubForms do an Async post pack for the filtering of my dropdown lists. I have cut down quite a bit of existing CS code here to show you only the new code in the context of some of the key CS elements. Keep in mind, the state list drop down is just a filter on my stores, and is not saved.



<asp:ScriptManager ID=”ScriptManager1″ runat=”server” />

<CSControl:CreateUserForm>

<
FormTemplate>
{Some existing CS code here}
{Call out to my SubForms below…}

<tr>
<td class=”CommonFormFieldName”>
I am a:
</td>

<td class=”CommonFormField”>
    
    
<ProactiveLogicSubForms:StoreAffiliationTypeSubForm ID=”StoreAffiliationTypeSubFormID” runat=”server” StoreAffiliationTypeId=”StoreAffiliationID”>
    
<FormTemplate>
         
<asp:DropDownList ID=”StoreAffiliationID” runat=”server” /><asp:RequiredFieldValidator id=”StoreAffiliationTypeValidator” runat=”server” ControlToValidate=”StoreAffiliationID” Cssclass=”validationWarning“>*</asp:RequiredFieldValidator
     
</FormTemplate>
    
</ProactiveLogicSubForms:StoreAffiliationTypeSubForm>

</td>
</tr>

<tr>
<td class=”CommonFormFieldName”>
of the Store:
</td>

<td class=”CommonFormField”> 
     
<ProactiveLogicSubForms:StoreAffiliationSubForm ID=”StoreAffiliationSubFormID” runat=”server” StateListID=”StateListID” StoreListID=”StoreListID”> 
     <
FormTemplate>
    
    
<asp:UpdatePanel ID=”UpdatePanel1″ runat=”server”>
    
<ContentTemplate>            


            <asp:DropDownList ID=”StateListID” runat=”server” AutoPostBack=”true” /><asp:RequiredFieldValidator id=”StoreAffiliationStateValidator” runat=”server” ControlToValidate=”StateListID” Cssclass=”validationWarning”>*</asp:RequiredFieldValidator>


            <asp:UpdateProgress ID=”UpdateProgress1″ runat=”server” AssociatedUpdatePanelID=”UpdatePanel1″ DynamicLayout=”true” DisplayAfter=”0″ EnableViewState=”false”>
                <ProgressTemplate>Loading Stores <br /></ProgressTemplate>
            </asp:UpdateProgress>


<br />


            <asp:DropDownList ID=”StoreListID” runat=”server” /><asp:RequiredFieldValidator id=”StoreAffiliationValidator1″ runat=”server” ControlToValidate=”StoreListID” Cssclass=”validationWarning”>*</asp:RequiredFieldValidator>


      </ContentTemplate>
      </
asp:UpdatePanel>


      </FormTemplate
      </ProactiveLogicSubForms:StoreAffiliationSubForm>
</td>
</tr>


Editing the new User Properties in the Control Panel


In order to edit the user, I added a new “Store” tab to the edit profile page with a call out to my sub forms in the file “Web\Themes\default\User\edituser.aspx”.  The code is pretty much identical to the create user form, with the addition of a tab. 



<CSControl:EditUserForm SubFormIds=”AvatarSubForm,StoreAffiliationSubFormID,StoreAffiliationTypeSubFormID”>  

<TWC:TabbedPane runat=”server”>

<Tab>Store</Tab>
<Content>
<table>
<tr>

<td class=”CommonFormFieldName”>
I am a:
</td>

<td class=”CommonFormField”>
    
    
<ProactiveLogicSubForms:StoreAffiliationTypeSubForm ID=”StoreAffiliationTypeSubFormID” runat=”server” StoreAffiliationTypeId=”StoreAffiliationID”>
    
<FormTemplate>
         
<asp:DropDownList ID=”StoreAffiliationID” runat=”server” /><asp:RequiredFieldValidator id=”StoreAffiliationTypeValidator” runat=”server” ControlToValidate=”StoreAffiliationID” Cssclass=”validationWarning”>*</asp:RequiredFieldValidator
     
</FormTemplate>
    
</ProactiveLogicSubForms:StoreAffiliationTypeSubForm>

</td>
</tr>

<tr>
<td class=”CommonFormFieldName”>
of the Store:
</td>

<td class=”CommonFormField”> 
     
<ProactiveLogicSubForms:StoreAffiliationSubForm ID=”StoreAffiliationSubFormID” runat=”server” StateListID=”StateListID” StoreListID=”StoreListID”> 
     <
FormTemplate>
    
    
<asp:UpdatePanel ID=”UpdatePanel1″ runat=”server”>
    
<ContentTemplate>             

            <asp:DropDownList ID=”StateListID” runat=”server” AutoPostBack=”true” /><asp:RequiredFieldValidator id=”StoreAffiliationStateValidator” runat=”server” ControlToValidate=”StateListID” Cssclass=”validationWarning”>*</asp:RequiredFieldValidator

            <asp:UpdateProgress ID=”UpdateProgress1″ runat=”server” AssociatedUpdatePanelID=”UpdatePanel1″ DynamicLayout=”true” DisplayAfter=”0″ EnableViewState=”false”>
                <ProgressTemplate>Loading Stores <br /></ProgressTemplate>
            </asp:UpdateProgress>

<br /> 

            <asp:DropDownList ID=”StoreListID” runat=”server” /><asp:RequiredFieldValidator id=”StoreAffiliationValidator1″ runat=”server” ControlToValidate=”StoreListID” Cssclass=”validationWarning”>*</asp:RequiredFieldValidator

      </ContentTemplate>
      </
asp:UpdatePanel

      </FormTemplate
      </ProactiveLogicSubForms:StoreAffiliationSubForm>
</td>
</tr>


</table>
</Content>
</TWC:TabbedPane>


SubForms Implementation    


I created a new project to implement my SubForms in and added a project reference to it from CS. I also updated the web.config in CS to know about my new controls by adding the following:



<controls>
<
add tagPrefix=ProactiveLogicSubForms namespace=ProactiveLogic.CS.SubForms assembly= ProactiveLogic.CS />


Now implementing the SubForms is a little tricky because my SubForms are used in both an update and creation context. In order to do this I always look for “posted” form values first and if there are no posted values, I see if I can get a CS User object to pull the values from for display. My code here is actually doing a lookup based on the state that is selected in order to populate the list of stores in that state. I am only showing my store selection SubForm as it is the more complicated of the two subforms that I created and should give you the most to chew on.



using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using CommunityServer.Controls;
using CommunityServer.Components;
using ProactiveLogic.WebFacade;
using ProactiveLogic.Entities;    

namespace ProactiveLogic.CS.SubForms
{

public partial class StoreAffiliationSubForm : WrappedSubFormBase

  private DropDownList StateList;
  private DropDownList StoreList; 

  // this ID is set in the ProactiveLogicSubForms:StoreAffiliationSubForm markup to
  // provide an ID reference to the state list dropdown

  public string StateListID
 
{
   
get { return (string)(ViewState[“StateListID”] ?? “”); }
    set { ViewState[“StateListID”] = value; } 
  } 

  // this ID is set in the ProactiveLogicSubForms:StoreAffiliationSubForm markup to
  // provide an ID reference to the store list dropdown

  public string StoreListID
  {
    get { return (string)(ViewState[“StoreListID”] ?? “”); }
    set { ViewState[“StoreListID”] = value; }
  } 

  public override bool IsEnabled()
  {
    return true
  }    

  protected override void AttachChildControls()
  {
    // view state is disabled, so populate the state list every time
    StateList = CSControlUtility.Instance().FindControl(this, StateListID) as DropDownList;
    this.StateList.Items.Add(new ListItem(“<Select Store State>”, “0″)); 

    foreach (State state in CachedData.GetAllUSStates())
    {
      this.StateList.Items.Add(new ListItem(state.StateName, state.StateId.ToString()));
    } 

    // attach the Store list member - the Store list depends on the statelist value
    // which is not bound yet - so we will populate that in the DataBind method 
    StoreList = CSControlUtility.Instance().FindControl(this, StoreListID) as DropDownList
  }    

  public override void DataBind()
  { 

    // determine the selected Store: always use posted form values if they are available -
    // then use the existing user’s values if they are available 
    int selectedStore = 0;
    int.TryParse(this.Page.Request[StoreList.UniqueID], out selectedStore);

    if (selectedStore == 0)
    {
       // if no store is selected yet, try to load the value from an existing user object
       // if it is available
       // (CS passes the User object to subforms if it is available in the context of what the user is doing)
       User ExistingUser = this.DataSource as User;
       selectedStore = (ExistingUser == null) ? selectedStore : ExistingUser.StoreAffiliatedToId; 
       // Select the state for the seleted store
       StateList.SelectedValue = CachedData.GetStoreByID(selectedStore).StateId.ToString(); 
    

    // repopulate the Stores based on the selected state
   
this.StoreList.Items.Clear();
    this.StoreList.Items.Add(new ListItem(“<Select Store>”, String.Empty));
   
    foreach
(Store col in CachedData.GetAllStoresForState(int.Parse(StateList.SelectedValue)))
    {
      this.StoreList.Items.Add(new ListItem(string.Format(“{0} ({1})”, col.Name, col.Town), col.StoreId.ToString()));
    }   

    // select the store – which was either loaded from a post value or
    // user object value above
    if
(StoreList.Items.FindByValue(selectedStore.ToString()) != null
    {
       StoreList.SelectedValue = selectedStore.ToString();
    }
  }


  // CS calls this to get the UI values into the Data object to save 
  public
override void ApplyChangesBeforeCommit(object activeObject)
  { 

    base
.ApplyChangesBeforeCommit(activeObject); 

    User user = activeObject as User;
    if (user != null)
    {
      // Just save the store – the state is just a filter and does not need to be saved
      user.StoreAffiliatedToId = int.Parse(this.StoreList.SelectedValue);
    }
 
}


  public override void ApplyChangesAfterCommit(object activeObject)
  {
     base.ApplyChangesAfterCommit(activeObject); 
  }
 
}
}


Displaying the Data In the Profile


For the displaying the new fields in the User Profile I added callouts to my new fields in the file: “Web\Themes\default\User\userprofile.aspx”


I added my fields under the “GMT” display. These fields query the aforementioned UserProfileData.GetPropertyValue method, based upon the “Property” value.



<CSControl:UserProfileData ID=”UserProfileData2″ runat=”server” Property=”storeaffiliationtype”>

<LeaderTemplate>
<tr>

<td class=”CommonFormFieldName”>
I am a:
</td>

<td class=”CommonFormField”>
</LeaderTemplate>

<TrailerTemplate>
</td>
</tr>
</TrailerTemplate>

</CSControl:UserProfileData>

<CSControl:UserProfileData ID=”UserProfileData1″ runat=”server” Property=”storeaffiliation”>

<LeaderTemplate>
<tr>
<td class=”CommonFormFieldName”>
of the Store:
</td>
<td class=”CommonFormField”>
</LeaderTemplate>

<
TrailerTemplate>
</td>
</tr>
</TrailerTemplate>

</CSControl:UserProfileData> 


Displaying the data in Forum Posts


Forum Flat View


The flat view of a forum is contained in the file: “Web\Themes\default\Forums\thread-flatview.ascx”

Under the line that has the user Avatar, I added a callout to my new fields.  Notice how easy it is to display your new fields. 



<CSControl:UserAvatar runat=”server” BorderWidth=”1″ Tag=”Li” CssClass=”ForumPostUserAvatar” />

<CSControl:UserProfileData ID=”StoreAffiliatedTo” runat=”server” Property=”storeaffiliation” Tag=”Li” CssClass=”ForumPostUserAttribute” />

<CSControl:UserProfileData ID=”StoreAffiliationType” runat=”server” Property=”storeaffiliationtype” Tag=”Li” CssClass=”ForumPostUserAttribute” />


Forum Threaded View


The post view for the threaded view of a forum is in the file: “Web\Themes\default\Forums\post-threadedview.aspx”

I added my new fields under the display name.  Notice that while the Forum flat view uses a list to display the user values, the threaded forum view uses container divs. 



<div class=”ForumThreadPostAuthor”><CSControl:UserData runat=”server” LinkTo=”Profile” Property=”DisplayName” /></div>

<div class=”ForumThreadPostPubDate”><CSControl:UserProfileData ID=”StoreAffiliatedTo” runat=”server” Property=”storeaffiliation” /></div>

<div class=”ForumThreadPostPubDate”><CSControl:UserProfileData ID=”StoreAffiliationType” runat=”server” Property=”storeaffiliationtype” /></div>


Conclusion


I spent about 8 days @ 1.5 hours a day to come up with what is in this post. It took another 3.5 hours to write this post (edit: about 3 more hours to format and revise the post). I’m hoping that this helps you extend CS much faster than that. I basically just loaded the code and had at it. I’m very open to suggestions on how to improve this, but I wanted to get this posted because it works for me and should hopefully help some of you who want to dig in and extend CS. I think CS seems to be a great platform and I am very impressed with how factored and clean the code is. The extensibility at the data layer seems to be an area that can be improved, but now that I typed up these instructions, hopefully it is a day of work or less to extend the CS schema so that you can capture, view and query your user data efficiently.


Cheers,
Jon


  

Packaging a ClickOnce Server Deployment

Saturday, September 30th, 2006

Recently, I have helped one of our clients with ClickOnce deployment. Part of the system that our client sells consists of application servers that have a .Net web service, and a smart client application. We helped our client improve the architecture of the web service and the Smart Client application to support extensibility for customizations specific to their customers. These customizations are out of the scope of this post, but during this re-architecture, the technology base was upgraded from VS 2003 & .Net 1.1 to VS 2005 and .Net 2.0.

The extensibility, plug-in and pipelining portions of the project went over very well. We did however face an unforeseen complexity: ClickOnce Deployment. We had done our fair share of reading on the benefits of ClickOnce and had read some of the deeper technical articles, that all focused on ClickOnce deployment from the web server to the client. The benefits seemed great on paper, even though there are issues with Xbrowser support that quickly rear their head. Also, there seems to be a lack of documentation around actually getting the server portion of ClickOnce installed, and that is what this post focuses on.

ClickOnce deployment has two very important files: the application manifest that defines what files make up the application and the deployment manifest that has the deployment version information and the path (the ProviderURL) for the ClickOnce application to look for updates from. These manifests are signed and this leads to a very tough issue to get around: how can you have a setup package that contains a deployment URL that you do not know ahead of time?

Our client sells their web applications to many customers. These servers are all imaged with Windows Server 2003 and the .NET framework 2.0. Then a deployment team installs the web service and ClickOnce application on the server via an MSI setup package. In the field, servers are always added to server farms to increase capacity. The need for quick setup of machines via a setup package is required.

The nature of the client application that we built, includes dynamically loaded assemblies in order to support customizations of the core application. This scenario is not covered by the “right click -> deploy” ClickOnce deployment method in Visual Studio or MSBuild. When using MSBuild /deploy, the ClickOnce setup package only consists of statically linked assemblies.

What we did was prepare a nAnt script for the build team to use, which creates the proper setup program, including dynamically linked files and additional files needed for server setup. The setup program itself is created via a Web Setup project in Visual Studio.

The nAnt script that we created performed the following steps:

  • Get files from source safe and label
  • Update all assembly info files with version information (remember that the assembly version is not what is used by ClickOnce to determine if there is a new version to download, the deployment manifest is used for that purpose)
  • Build all of the assemblies that are statically and dynamically linked.
  • Create the application manifest using Mage.exe, linking all of the statically and dynamically linked assemblies
  • Create the deployment manifest and version it with Mage.exe – ensure that the deployment provider URL points to an invalid host name
  • Sign the Deployment and Application manifests with a certificate that is not issued by a certificate authority
  • Copy all of the files that are needed into a temporary build area with the Web Setup Project
  • Update the default web page with version information
  • Include a vbscript that is invoked during the install that will update the manifests (more on this later)
  • Invoke the Web Setup package via devenv (you need Visual Studio 2005 installed on the build machine)

After the NAnt script is run, there will be a setup.exe and MSI install package for your ClickOnce server deployment. In this package a dummy certificate is included and a vbscript for updating the deployment and application manifests.

In the Web Setup project we configured the vbscript to run during the server install. The vbscript prompts the user to input the URL that will be used for ClickOnce client updates. This URL is then used to update the deployment manifest with the new deployment URL. The prompt also tells the installer that they should point to the load balanced hostname or as opposed to the specific server IP or hostname. The vbscript invokes mage.exe to perform this step. Mage.exe is part of the .Net Framework 2.0 SDK and must also be deployed with the setup project.

mage.exe -Update myclickonceapp.application -ProviderUrl http://myserverhostname/myclickonceapp

The installer then prompts for a path to the certificate to sign with. Customers can use their own certificate to sign the application manifests with, or they can use the unsecured dummy certificate that is included in the setup package.

mage.exe -Sign myclickonceapp.application -CertFile somecertificate.pfx -Password somepassword

Next the installer vbscript must update the bootstrapper that is used to install the prerequisites. This step is needed because the bootstrapper itself will invoke the ClickOnce application after it installs the prerequisites. Note, that there are even issues around this if the client browser is not IE. There are some very good work arounds for ClickOnce Firefox issues discussed over on the Microsoft Channel 9 forums.

setup.exe /url= http://myserverhostname/

After this is complete, the vbscript is left on the server in case if the user wants to update the deployment URL after setup.

This build and installation has worked very well for us, but you do need to at least provide instructions on the setup web page for Firefox users on how to properly install the application, or follow some of the work arounds detailed on the Microsoft Channel 9 Forums.

I hope this helps
-Jon


 
 
Home | About Us | Services | Products | Blog | Contact  | Search
Copyright 2005-2006 Proactive Logic LLC. All rights reserved.