Introduction
This article is the second part in the series: Building a Web Message Board using Visual Studio 2008. The first part in the series can be accessed here:
Contents
This article demonstrates the following features of Visual Studio 2008:-
- Client Application Services - ASP.NET 2.0 introduced membership, profile, and role services. Membership allows you to maintain site users, profile allows you to store per user personalization data, and roles allow you to designate one or more roles to the users for role-based security purposes. ASP.NET AJAX allows you to access these services from JavaScript in a web application. With Visual Studio 2008, these services are accessible from any client application: Windows Forms, WPF, or Office. The client application can access these services on a remote web server using an easy to use API.
- Office 2007 Document Templates - Visual Studio 2008 Tools for Office allows you to associate an assembly with an Office 2007 document or a document template. The assembly can control or access various portions of the document and add customized behavior. For example, in this article, we will see how we can extract subject and message text from specific regions in a Word document and post to a message board.
Let's get started by looking at what we build in the article, and then we can see the steps involved in building it.
In this article, we will be building a Microsoft Word document template. Users can create Word documents using this template. The Word document, so created, will have place holders where the user can type in a message subject and a message text. Once the user is done with typing the message, he can click on a ribbon button labeled Post Message. This causes the message to be posted on the MessageBoard website. The following screenshot demonstrates all the components:
The Post Message ribbon button is only available for documents developed using the custom document template. Any other Word document will appear in the normal fashion, i.e., it will not have the Post Message button. Before proceeding, let's look at the reasons why we are going through the trouble of building a custom Word document template.
It has happened to me many times that I prepare an elaborate post for a message board, such as that in the CodeProject website or the www.asp.net website, and I accidentally close the browser, or get disconnected from the internet, or accidentally navigate to a different site. The elaborate message I prepared gets lost, and I have to either re-type the entire thing or not post the particular message. So, the solution I adopted (and which many other people have adopted) is that I type the message separately in Word or Notepad and then copy and paste the message text to the appropriate form fields in the message posting page. Then I submit the form to the website. The advantage of using Word is that I can save the message offline and also get all the good features such as spelling and grammar correction. The code presented in this article provides the users with a custom document template that they can use to post a message to the MessageBoard website developed in the previous article. The users can directly post the message from Word, and they don't have to cut and paste the message contents to form fields in a web page.
Microsoft Word 2007 also has a built-in document template for blog posts. The blog post document template can post to any site that implements the meta-blog API. For the message board, one solution is that we can implement the meta-blog API and allow users to directly use the blog post word template. We will do so in one of the later articles. In this article, I want to keep things simple and also demonstrate how to build custom document templates.
Before we get started with actual code, let's look at the different types of Microsoft Office Word solutions we can develop in Visual Studio 2008.
Microsoft Office Word is a highly customizable application. VBA (Visual Basic for Applications) was available to Office Developers for a long time. VBA allowed developers to automate certain tasks in Microsoft Word such as formatting ranges, customized search and replace etc. The VBA code was saved with Microsoft Word documents or the templates. VBA macros only worked for a particular document or a template. For application level customization, Word has support for Add-Ins. Word Add-ins can be COM based, hence can be developed in Visual C++, Visual Basic 6.0, or even Managed code.
Visual Studio Tools for Office (VSTO) provides an easier, better, and secured way to create Word solutions in managed code. The VSTO runtime shields most of the COM details, and provides a more .NETish way to develop Word solutions. Also, VSTO solutions run in a separate app-domain, and therefore they provide a good degree of isolation among different solutions. VSTO has support for the following:
- Application-level Add-Ins - You can use VSTO to develop Word add-ins. These add-ins are not associated with a particular document, and can be used to provide generic services such as document sharing and collaboration and source control.
- Document-level Customizations - VSTO can also be used to customize a Word document. VSTO allows you to develop a .NET assembly and associate it with a Word document. The .NET assembly can be used to add custom behavior to the Word document.
- Document-template-level Customizations - If you want to customize a class of documents (documents having common formats, behavior, or contents), you can customize a Word document template using VSTO by associating a managed assembly with the template. The customization is automatically available to all the documents created from the template.
Now that we have seen different solutions that can be created using VSTO, let's select the right solution for the message board application.
For the message board application, we can theoretically use any of the three types of solutions supported by VSTO. We can build an add-in that allows documents to be posted to the message board. However, the problem with an add-in is that it will be available for all the documents. The format of the document we want to use to post to the message board is mostly fixed: it has an area where the user can type the subject and an area where the user can type in the message text. Thus, it does not make much sense to develop an application level add-in. We can either develop a customized Word document or a document template. The customized document template makes more sense for the following reason:
- Users can post different messages to the message board. The structure and format of the document will be the same but the contents of each message will be different. The best way to enforce the structure and format is to use a template.
- Users will mostly start off with an empty document with place holders, and type the message. It does not make any sense to have any concrete content in a document, which is another point in favor of using a document template.
Now that we have decided to use a document template, let's get our hands dirty with some coding and jump right into development.
In this article, I will be working with Word 2007. The procedure for developing a Word 2003 template will differ slightly. The main difference is that Word 2003 does not use ribbons. Depending on the requirements in a real project, you may have to support both Word 2003 and Word 2007. If you are interested in learning how to develop for multiple versions of Word, please leave a comment.
To start, download the code from Part I of the article, by clicking on this link, and follow the installation instructions to install the back-end database.
Next, extract the zip file and open the MessageBoard.sln file in Visual Studio. Right click on the solution, and click on Add New project. From the project dialog box, select:
This will cause the VSTO Wizard to appear:
Type MessageBoard as the name of the document (as shown), and click OK. This will cause Word to open within the Visual Studio window, as shown below:
You can drag and drop controls from the tool box to the Word document. Next, we will add controls to the Word document.
You can drag and drop and use most of the Windows Forms controls in a Word document. In this project, however, we will be using a special type of controls called Word controls:
These controls are also called content controls. Content controls can be used to develop form like input within a Word document. Content controls can be used to designate different areas in the Word document for a specific purpose. For example, in the message board document template, we want to designate an area for entering the message subject and the message text. The control we will use is the PlainTextContentControl
. The PlainTextContentControl
, as the name suggests, can only contain plain text, sort of like the Windows Forms TextBox
control. In our message board website, we don't have support for rich text content yet (we will be adding it in a later part in the series), so it makes sense to use the plain text control. We will drag and drop two plain text controls, one for the subject and one for the message text. Just like Windows Forms controls, we can provide a name to each of the content controls. In our example, we will name them subject
and messageText
. There are some other properties of the controls that are useful to us:
Property Name | Description |
---|
Text | Represents the text of the control. We will set the value of the property to empty at design time. This property will be used at runtime to access the subject and the message text. |
PlaceHolderText | Represents an instructional text that appears to the user when the control has no text. This text appears in a slightly grayed color:
|
Title | The title appears on top of the control, and provides a quick clue on what a control does:
|
Here is how the properties are set for the subject
control:
We also format the subject
control as Heading 1
.
Here is how the properties look like for the messageText
control:
Now that we have designated areas in the document for subject and text, we need to provide a means to the end user by which he can post the text entered in the content controls to the message board. In Office 2007, a user invokes commands using the Ribbon. In the next section, we will add controls to the Ribbon so that the user can post messages.
Prior to Visual Studio 2008, adding controls to the Ribbon was done by hand editing XML files. Visual Studio 2008 includes a graphical designer for designing Ribbon controls.
Also, note that adding controls to the Ribbon for a document template project works only for Microsoft Word. It does not work for Excel. The reason behind this is that Word is not an MDI application as Excel. Word has multiple top level documents, and each of the documents have their own Ribbon. Excel, however, has a single Ribbon for all the documents.
To add a Ribbon, right click on the project and select Add Item. Select Ribbon from the list of items, as shown below:
The Ribbon appears in the designer as shown below:
The Ribbon tab consists of Ribbon groups, and each Ribbon group consists of Ribbon controls, as shown. Microsoft Word has predefined Ribbon tabs such as Home, Insert, Page Layout, Add-ins etc. The Ribbon created using the designer in the above screenshot will merge with the existing controls in the pre-existing Ribbon tab named Add-Ins. However, in our message board sample, we want the control to appear in the Home tab. To do so, we need to modify the properties of the tab. Click on the ribbon tab in the designer, and modify the properties as shown below:
We changed the OfficeId
property to TabHome
. This will make all controls and groups placed inside the tab to be merged with the Home ribbon tab. The question here is how did we get the OfficeId
for the home tab. An Excel sheet available at the Microsoft Office website has a list of IDs of all the standard Ribbon controls for all the Office applications. You can use the Excel sheet to find the IDs of the standard Ribbon tabs, groups, and controls. You can download the Excel sheets for all Office applications, from the following link:
2007 Office System Document: List of Control IDs.
Now, if you run the application, the ribbon group will correctly appear in the Home tab; it will, however, appear at the extreme right. In order to make it appear in the left, we have to modify the properties of the group as shown:
The important property to note here is the PositionType
property. We set it to BeforeOfficeId
to indicate that the group should appear before a standard Office group. We set the OfficeId
property to GroupClipboard
to place it before the clipboard group. Again, the ID of the clipboard group was obtained using the Excel sheet mentioned above.
Now, we have the ribbon group appearing in the correct tab at the correct position. Next, we need to add a button to the group.
You can add a button to the ribbon by dragging and dropping a Button
control from the Office 2007 Ribbon Controls tab in the toolbox:
A button in the ribbon has text, an ID, and an icon image. You can either specify a custom icon image for a button, or use some standard images which ship with Office. The list of standard office image IDs is available in an Excel worksheet available here.
In the case of the message board, I use the same icon that the default blog template uses for posting a message. The icon ID is BlogPublish
. Finally, here are the properties of the newly added button:
Now that we have a button, we can add code to handle its Click
event. Double clicking the button automatically generates a handler. This handler is in the Ribbon.cs file. We add code as follows:
private void post_Click(object sender, RibbonControlEventArgs e)
{
Globals.ThisDocument.PostMessage();
}
The Globals.ThisDocument
gives the reference to the Word document object that is associated with the assembly by the code. VSTO loads all assemblies in a separate AppDomain and also does a shadow copy, so even if there are multiple instances of the template in a single Microsoft Word process (winword.exe), Globals.ThisDcoument
will still give the correct document reference. The static variables are per AppDomain. Finally, we add a method called PostMessage
with the following code:
internal void PostMessage()
{
if (!ValidateSubjectAndText())
return;
}
We first validate the subject and the text to make sure that they are not empty. If the validation is successful, we call a method called AuthenticateUser
which authenticates the user, and finally, we invoke the web service that posts the message, using the InvokeWebService
method. We will get into the details of AuthenticateUser
and InvokeWebService
a little later. Here is how the ValidateSubjectAndText
method looks like:
private bool ValidateSubjectAndText()
{
if (String.IsNullOrEmpty(subject.Text))
{
MessageBox.Show(Resources.SpecifySubject, Resources.ApplicationName);
return false;
}
if (String.IsNullOrEmpty(messageText.Text))
{
MessageBox.Show(Resources.SpecifySubject, Resources.ApplicationName);
return false;
}
return true;
}
We have not yet added the actual code to post the message to the message board. Before we do that, we need to expose the message board API as a web service. This web service can then be used by the document template to post the message. Now, let's move on to the server side.
We need to add a web service that can be used by the document template project to add messages to the message board. To add a message to the message board, we use the AddMessage
method of the MessageSource
object:
public static void AddMessage(string subject, string text)
The details of the method are in the previous article in the series. We have to expose a web service that, in turn, uses this method to add the message to the message board. The first question is what kind of web service should we create? We have two options: ASP.NET (asmx) web services or WCF Web services. In the next section, we will decide which one to use.
Prior to WCF, ASP.NET Web services (asmx) was the common way web services were built in .NET. ASP.NET Web Services are not as rich as WCF. For instance, WCF has support for most of the WS Specs. The neat thing about WCF is that you can get rich web service features just by changing the configuration file. Although we will not be using a lot of WCF features in this article, it still makes sense to expose the message board as a WCF service as it will be easy to to have a rich web service support in the future. In the next section, we will create the WCF service.
Following the conventions of the previous article, we will add the actual code for the WCF service in the MessageBoard.Web project and add the svc file that exposes the web service to the MessageBoard website. The easiest way to create a web service is to use the Add New Item dialog box:
This automatically creates the Service contract - IMessageBoardService
and a class that implements this contract - MessageBoardService
. We modify the wizard generated interface, IMessageBoardService
, as follows:
[ServiceContract]
public interface IMessageBoardService
{
[OperationContract]
void AddMessage(string subject, string text);
}
Next, we need to modify the MessageBoardService
class to implement this interface, as follows:
[AspNetCompatibilityRequirements(RequirementsMode
=AspNetCompatibilityRequirementsMode.Allowed)]
public class MessageBoardService : IMessageBoardService
{
public void AddMessage(string subject, string text)
{
MessageSource.AddMessage(subject, text);
}
}
Notice, the AspNetCompatibilityRequirements
which is set to AspNetCompatibilityRequirementsMode.Allowed
. This attribute works in conjunction with the following configuration setting in the configuration file:
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
Combined together, these properties ensure that the HttpContext.Current
property returns a valid object when called from within the service code. More about this can be found in this blog post. Why do we need ASP.NET compatibility? AddMessage
calls the Membership.GetUser
method. The GetUser
method internally uses HttpContext.Current
to find the user. It has also to do with how we are authenticating the user; we will be using .NET Client Application Services as opposed to the built-in WCF authentication.
The next step is to add an svc file which will be used to access the WCF service. We create a file named MessageBoard.svc in the MessageBoard website:
<%@ ServiceHost Service="MessageBoard.Web.MessageBoardService" %>
The easiest way to do it is to add a text file to the website and rename the extension to svc. We are not fully done with the web service yet. We need to add configuration settings to the web.config file.
WCF services need to be configured before they can be used. You can either use the WCF Service Configuration Editor, or hand edit the web.config file. Either way, you need to add the following settings to the configuration file:
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
<behaviors>
<serviceBehaviors>
<behavior name="MessageBoardServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="MessageBoardServiceBehavior"
name="MessageBoard.Web.MessageBoardService">
<endpoint binding="basicHttpBinding"
bindingConfiguration=""
contract="MessageBoard.Web.IMessageBoardService" />
</service>
</services>
</system.serviceModel>
Note: the system.servicemodel
section may already be present in the configuration file. In that case, you need to merge the above configuration settings with the already existing contents in the web.config file.
We are exposing the service using BasicHttpBinding
, we are not using the more secure WsHttpBinding
to keep things simple. Remember, though, that we can always opt to use another binding(s) just by changing the configuration settings. Now, we have the web service configured correctly. You can test this by viewing the MessageBoard.svc file in the browser.
Now, let's go back to the document template project and see how we can invoke the web service.
In order for us to invoke the web service from the client project, we need to add a reference to the web service. This can be done by right clicking on the MessageBoard.Word project and selecting Add Service Reference. In the Add Service Reference dialog box that shows up, click on the Advanced button on the lower left corner. In the dialog box that pops up, click the Add Web Reference button. This will bring up the Add Web Reference dialog box. In the resulting dialog box, click on web services in the current solution and select the MessageBoardService. Once you select the MessageBoardService, the proxy class to invoke the service is automatically created for you.
Why is adding a Web Reference so complex?
What used to be a single step in Visual Studio 2005 is now three steps in Visual Studio 2008. The reason behind this is that Visual Studio 2008 wants you to use WCF for web service client code. We cannot use WCF client code as it does not provide as much control over the web request as the older web service proxy class did. In this article, we need some control over specifying the cookies when invoking the web service. This is not possible if we are using the WCF based client code, hence we have to use the older web service proxy.
Now, we are ready to invoke the web service.
We added a place holder method named InvokeWebservice
in the ThisDocument.cs file to invoke the web service. This can now be coded as follows:
private void InvokeWebService()
{
using (MessageBoardService service = new MessageBoardService())
{
try
{
service.AddMessage(subject.Text, messageText.Text);
MessageBox.Show(Resources.MessagePosted,
Resources.ApplicationName);
}
catch (WebException ex)
{
MessageBox.Show(ex.Message, Resources.ApplicationName);
}
catch (SoapException ex)
{
MessageBox.Show(ex.Message,
Resources.ApplicationName);
}
}
}
Uncomment the InvokeWebService
call in the PostMessage
method:
internal void PostMessage()
{
if (!ValidateSubjectAndText())
return;
InvokeWebService();
}
To test launch the MessageBoard.Web project, enter some subject and text, and click on the Post Message button in the ribbon. If the message is posted successfully, you will get a message box indicating that the message was posted successfully. You can also view the message board website in the browser to make sure that the message appears. If everything worked correctly, you will notice that new messages are posted anonymously. That is the case because we have not yet authenticated the users. In the next section, we will use ASP.NET client application services to authenticate the user.
If you have been using WCF, you might be wondering why we are not using the WCF authentication in this article. WCF security is quite powerful, and it also integrates with ASP.NET membership. However, WCF authentication requires extra configuration work. For example, if you want to use user name/password authentication in WCF, you need to configure a certificate (or a test certificate) in order to make the authentication work. Although such high security may be needed for many projects, I decided to keep things simple and use a light weight alternative: ASP.NET Client Application Services.
What are the ASP.NET Client Application Services? Put simply, ASP.NET Client Application Services allow you to access the membership, profile and roles services on an ASP.NET web server remotely from any .NET application. So, if you have a web server configured with ASP.NET membership, you can use the web server to authenticate a user on a desktop Windows application. This is an extremely lightweight API for authentication. If you recall, the MessageBoard site was configured with ASP.NET membership, and users were authenticated using ASP.NET membership. Now, we need to do the same for the messages posted from Microsoft Word.
To achieve authentication using ASP.NET membership, first, we need to configure the server. Add the following settings to the web.config file:
<system.web.extensions>
<scripting>
<webServices>
<authenticationService enabled="true"/>
</webServices>
</scripting>
</system.web.extensions>
Next, we need to modify the project properties of the MessageBoard.Web project:
Under the Authentication service location, enter the URL of the MessageBoard website. Also notice the Credentials provider field set to MessageBoard.Word.LogOnForm
. This is the name of a very simple
log-on form developed using Windows Forms, which implements an interface IClientFormsAuthenticationCredentialsProvider
. The code for the form is shown below:
public partial class LogOnForm : Form,
IClientFormsAuthenticationCredentialsProvider
{
public LogOnForm()
{
InitializeComponent();
}
public ClientFormsAuthenticationCredentials GetCredentials()
{
return (this.ShowDialog() == DialogResult.OK) ?
new ClientFormsAuthenticationCredentials(
userName.Text,
password.Text,
rememberMe.Checked)
: null;
}
}
Here is how the form looks in the designer:
When you authenticate the user using the Client Application Services, this dialog box will automatically be shown when the user name supplied to the authentication API is null or empty and if there are no saved credentials. The client application service automatically saves the user credentials in an offline SQL Server Everywhere database when indicated to do so (Remember Me is checked). The database is automatically created by default in the user's data directory. If you don't intend to save the user authentication data, you can do so by clicking the Advanced button in the Services page in the project properties. Now, let's see the steps involved in authenticating the user. Once the project properties have been set correctly, authenticating the user is as simple as calling the Membership.ValidateUser
method:
private bool AuthenticateUser()
{
if (!Membership.ValidateUser(String.Empty,
String.Empty))
{
if (MessageBox.Show(
Resources.InvalidUserNamePassword,
Resources.ApplicationName,
MessageBoxButtons.YesNo)
!= DialogResult.Yes)
return false;
}
return true;
}
The AuthenticateUser
calls Membership.ValidateUser
with an empty user name and password. This will cause the LogOnForm
to be shown automatically for the user to provide the credentials. The ValidateUser
method returns null
if the authentication was unsuccessful; in that case, we ask the user whether he wants to post the message anonymously. If yes, we return true
; otherwise (or if the user is authenticated), we return false
.
We are not done yet. We need to make sure that the web service is invoked with the authentication done by the ValidateUser
method. The code for the InvokeWebService
method changes slightly:
using (MessageBoardService service = new MessageBoardService())
{
ClientFormsIdentity identity =
Thread.CurrentPrincipal.Identity as ClientFormsIdentity;
if (identity != null)
service.CookieContainer = identity.AuthenticationCookies;
try
{
service.AddMessage(subject.Text, messageText.Text);
MessageBox.Show(Resources.MessagePosted,
Resources.ApplicationName);
}
catch (WebException ex)
{
MessageBox.Show(ex.Message,
Resources.ApplicationName);
}
catch (SoapException ex)
{
MessageBox.Show(ex.Message,
Resources.ApplicationName);
}
}
If the authentication succeeds, the Thread
's principal is set to the a principal which has an identity of type ClientFormsIdentity
. If the authentication succeeds, the server returns cookies which contain the ASP.NET Forms authentication ticket. In the subsequent calls, the client can just send the cookies to indicate the authentication data to the server. The cookies are available in the CookieContainer
property of the ClientFormsIdentity
object. We set the CookieContainer
property of the web service proxy to the same cookie container so that the web service gets invoked with the right authentication cookies. Thus, the web service call is now authenticated at the server. You can test it out by posting another message and verifying that the user name is correctly set in the newly posted message.
This concludes the second part in the article series. In the next part, we will go back to the website and add some AJAX support.
Please be free to leave comments about the article, especially if you think that the article is lacking. That will help me improve future articles.
- December 28, 2007 - Initially posted.
- December 31, 2007 - Added Series Navigation and Installation instructions.
- December 31, 2007 - Resolved issues with uploaded files