Applied C#
Chapter 3 discussed the merits of C# and gave a tour of the language that included small coding examples to explain specific language details. This chapter has a broader focus and shows you how all those isolated language constructs can be woven together to produce the new and exciting class of software we call people-oriented software. We have chosen an example application that demonstrates what is likely to be a very common development scenarioan application is built to seamlessly integrate with complementary Web services of another application. In keeping with the spirit of our book's title, Applied .NET, the following list highlights a variety of different aspects of .NET development that are demonstrated in this chapter:
Producing a Web service
Invoking a Web service
Using server-side controls and events
Implementing field validation using active server page (ASP) .NET validation controls
Implementing an ASP.NET custom control
Performing object serialization over the Internet
Using various common language runtime (CLR) classes
Applying .NET exception handling
ManagedSynergy
In Chapter 2, we introduced the InternetBaton application, which allows people to collaborate on a shared resource in a decentralized fashion over the Internet. In this chapter, we create a new application called ManagedSynergy that makes use of the services provided by InternetBaton to create a decentralized document management system.
The Vision
ManagedSynergy is an application that combines document management features such as approval workflow with the distributed project folder capabilities of InternetBaton. The result is a document management system that does not require a centralized document server. This approach would be ideal for collaborative projects in which funds are tight and the participants have no real organizational relationship (which makes it tough to find shared servers to use). Charity work and certain Internet development projects are good examples in which this model would work well. In both cases the work being done is a labor of love but nonetheless needs to be done right with document management. The ultimate goal of this application is to provide an inexpensive means for people to practice managed collaboration in hopes that the end result is greater than the sum of the individual efforts.
The Functionality
The core concept in ManagedSynergy is the notion of a shared project file, which, not surprisingly, contains project items, each of which represents a shared InternetBaton resource. The focus of the application centers on manipulating the project file. Using ManagedSynergy, you can add and delete project items, check items in and out, view an item and its properties, and review an author's changes and either approve them or request that revisions be made. Checking an item out means that other authors participating in the shared project are not allowed to modify the item until it is checked back in. This is more a change management feature than it is version control because there is no notion of version history in this application.
Here is how it all works. When an author wants to make a change to the content of a project item, the first step is to check the item out so that others know it is being worked on. At this point, displaying the properties for that item would show that its revision status is checked out and its approval status is in revision. The author makes whatever modifications are needed and checks the item back in. At this point the revision status changes to checked in and the approval status (assuming that the review feature was enabled for this project) changes to in review. Each reviewer can look over the changes that were made and submit a brief review along with an indication of whether the changes were approved or further revisions are necessary. When all the reviewers have finished reviewing the changes, the approval status changes to either approved or revise depending on the verdict of each of the reviews. The changes are considered approved if all the reviewers agree to approve them; otherwise, the changes are rejected and revisions are required. To make things simple, all reviews are cleared each time changes are checked back in.
ManagedSynergy does not have its own administration facilities. Instead it uses those provided by InternetBaton. When project administrators want to set up a new shared project, they select the ManagedSynergy administration option, which takes them to the InternetBaton application where the project can be set up and participants can be identified. Once the project is set up, the users are no longer aware that they are using InternetBaton because its exposed Web services are programmatically accessed to seamlessly integrate the two applications. That is, when an item is added to the ManagedSynergy, it is also added behind the scenes to InternetBaton. The same holds true for checking out an itemit is reflected in both applications. When ManagedSynergy's view option is selected, the browser simply points to the Internet-Baton link, and the shared item is displayed.
Things are a little different when an item is reviewed. InternetBaton does not have the concept of reviewing items or establishing an approval process. This functionality resides solely in ManagedSynergy, and it is the added value this application brings to the table. In essence, what this application has done is to seamlessly extend the project file supported by InternetBaton and other than the administration angle, the user is truly unaware of which features ManagedSynergy provides and which features are provided by InternetBaton.
An interesting aspect of this application is that integration with InternetBaton is not just one way. ManagedSynergy operates in a multiuser environment in which several project participants may be working on their project items concurrently. Therefore it is necessary for project status to be dynamically updated as it changes. Because all project-related activity is mirrored to this Web service, InternetBaton becomes a natural choice for the central location from which to replicate out project changes to the other active project participants. This means that in addition to consuming Web services, ManagedSynergy will also expose Web services so that InternetBaton can call them to cause replication to occur. Another useful feature made possible as a result of exposing Web services is a synchronization feature that allows quite large files to be automatically updated overnight.
The Design
Chapter 2 introduced the concept of People-Types and how they could be used to help focus a developer's attention in the areas that are most important for people-oriented software. We continue with that approach in this chapter as we examine how the ManagedSynergy application addresses the primary forces that shape people-oriented software: universalization, collaboration, and translation. Because the process of using people-types involves stepping back and taking a look at how these forces factor into the design of an application, it may be helpful to first provide a concise list of this application's features.
Universalization
Now that we know what features the ManagedSynergy application provides, we can discuss those aspects of the design that resulted from taking a "miner's" perspective and determining how much of the functionality could be achieved through universalization. Universalization is ultimately concerned with identifying what existing resources can be utilized and applied in the design and implementation of an application. We consider the features in Table 4-1 one at a time to understand how all this worked.
Table 4-1 Features of ManagedSynergy
Feature |
Description |
Shared decentralized projects and files |
Allows files to be collaboratively developed by geographically distant participants without the need for a centralized document server |
Check-in, check-out, version status |
Allows files to be safely modified in a multiuser environment |
Document approval, publication, document status |
Allows management of changes to documents |
Offline document replication |
Allows the decentralized files to be replicated to each participant's machine overnight |
Real-time version and document status |
Allows status changes to be reflected in every active (running) instance of the ManagedSynergy client |
When we looked into the issue of a shared project file, we discovered that what was needed was the ability to save the current state of the project to disk. Further, we decided that if we could get the object that held the project data to serialize itself, we could have it write itself to disk and then later read itself back in with very little effort on our part. (In this context, laziness is a good thing.) Our miner-focused investigation determined that all this could be accomplished using .NET serialization.
Another interesting wrinkle related to object serialization was the fact that the project file itself needed to be a Baton item just like project items. Taking this approach, the project file could be easily shared between the participants. It would also ensure that the contents would always be accurate (because everyone would point to the same project files no matter where the current version was). Given this requirement, being able to serialize the project file over the Internet became a very desirable option and as it turned out, the CLR classes directly supported serialization over the Internet as well. We discuss serialization when we expand on ManagedSynergy's implementation later in this chapter.
When we thought about the issue of checking project items in and out, we found that one of the things that was needed was field validation. During the process of checking an item back in, the author needs to fill out a form that includes a check-in comment for others to refer to before they conduct their review. Because this comment is so important, ensuring that it was filled out became a requirement. Service-side controls provided field validationjust what the doctor ordered. As we thought about the other forms involved in the application, field validation was applicable in those instances as well, so once again our miner's perspective paid off.
Also in the area of forms management there was the desire to manage form control events in a familiar manner. ASP .NET and Visual Studio offer an event model that most programmers have already become accustomed to in Visual Basic and C++, so selecting this approach was an easy choice.
In terms of document publication, we had to make the movement of files from working directories to public directories automatic so that the users would not have to keep track of the details and to ensure that the copy took place when it should. It is not until the project items are copies to this public location (along with some necessary Baton collaboration) that they are visible to other participants.
As it turns out, offline document replication had very similar requirements to those of document publication in that files need to be copied from one place to another. Using the CLR's File and Directory classes made designing and implementing both of these application features a snap.
Throughout the design and implementation, we constantly had to deal with the unexpected. How would we deal with things not working because of external factors such as a full disk or a lack of proper file permissions? Our miner's perspective resulted in adopting .NET exception handling. (Okay. It was not that noteworthy or courageous a decision, but it was a decision born out of a miner's spirit and so I mention it here). Using this type of exception handling, we will even be able to receive exceptions thrown by the InternetBaton Web service, yet another application that we are calling over the Internet!
Collaboration
Collaboration affects how your application interacts with other applications and services to accomplish its goals. Continuing with the technique we learned in Chapter 2, we adopted a "conductor" perspective to help focus our attention on collaborative aspects of the application. In ManagedSynergy, collaboration is required to accomplish a seamless integration of InternetBaton functionality as well as support dynamic project status updates and offline document replication.
Integrating with InternetBaton involves calling its Web services to add, delete, view, check in, or check out project items. Each of these actions has an effect on the shared state of the project file as well as an effect on the state held in the InternetBaton application. For most of these actions, the general order of events is as follows:
Check out the project file from InternetBaton.
Deserialize the contents of that project file (just in case it changed since we started our session).
Apply the intended action (e.g., add, delete) to the project object.
Save the project object to disk using serialization, and copy the project file to its shared location.
Forward the action to InternetBaton, if appropriate.
Check back in the project file, thus making the change public.
In the scenario just described, ManagedSynergy takes an active role in the integration of the two applications. In contrast, implementing dynamic project status updates and offline document replication involves a passive approach (with respect to ManagedSynergy) in which the ManagedSynergy application itself is used as a Web service by the InternetBaton application. When a project item is checked in or out, the resulting status changes are sent to each of the participant's Web servers. Any participant who is currently using the application is dynamically notified of the change. Similarly, when InternetBaton has been instructed to perform offline document replication and it is time to perform the task, each participant's Web service is invoked and instructed to update the contents of each of the project items.
The new Web services capability provided by the .NET platform makes this kind of synergy very straightforward. Thinking about software as services has several positive implications and is something that will be one of the more important technologies being developed.
Translation
Translation is all about how identical concepts that may be dissimilar in form can be used in a heterogeneous environment. The translation story for ManagedSynergy is limited to the use of Web services. Web services in .NET are implemented using Simple Object Access Protocol (SOAP) technology; therefore any client that can speak SOAP can be a client of this application. InternetBaton could be ported to another operating system and the dynamic status notification would still work, as would the offline replication and the remaining interactions between the two applications. The point is that Web services allow clients to translate a SOAP request into a correct response no matter what kind of implementation was chosen for that client. This perspective shows that translation is of no small consequence.
The Implementation
Although this application's complexity is not on par with the software that launches the space shuttle, there is a reasonable amount of functionality in the ManagedSynergy application. What is surprising is that using .NET and our people-oriented techniques makes this implementation a rather simple one. In Chapter 3 we presented .NET as a revolution that elevates application development to a radically simpler level of implementation. Reviewing this application implementation should lend credibility to that statement. Developing in .NET is a pleasure, and your intuition about how things should operate is exactly how they do operate.
Before we get into the details of the implementation, we are going to discuss the application's main screen. Figure 4-1 is a screen shot of the Project.aspx page, the page on which the users spend the bulk of their time. Listed in the action bar are the various steps we outlined earlier in this chapter. Each of these actions originates from this page and returns back to it once completed. On the left-hand side of the page is the project item list (in this case, the chapters of a book that is being collaboratively developed). To the right of the project item list are the property details of selected items (in this case, Chapter 2.doc).
Figure 4-1 Project Page (Main Screen) for ManagedSynergy.
Probably the easiest way to examine this implementation is to describe a scenario that weaves through all the various actions that could be taken by a user. This will give us a basis for discussion as well as provide complete coverage of the various aspects of the implementation that need to be examined. The following list describes the various scenarios and the order in which we will cover each one:
- Opening an existing project
- Creating a new project
- Adding a project item
- Deleting a project item
- Checking out an item
- Viewing a project item
- Checking in an item
- Reviewing an item
- Viewing an item's properties
- Invoking administration services
Opening an Existing Project
We begin by discussing the steps involved in logging a user in to the application. Figure 4-2 shows the Start.aspx page, which users complete so that they can log in. The user ID is an e-mail address, and the project uniform resource locater (URL) must be linked to an InternetBaton resource that identifies the project file. If the project file does not exist, then users are alerted and asked if they want to create a new project file.
Figure 4-2 Start Page for ManagedSynergy.
We mentioned previously that server-side controls and field validation played a role in this implementation. The ASP .NET code for this page shows an example of both. Because all the other ASP .NET pages in this application involve the very same processes, we only need to discuss one example to explain how the presentation of this works. As pointed out in Chapter 2, ASP .NET introduces a way to cleanly separate the presentation code from the "code behind" the form. Therefore every form you see in this application has a presentation file (identified by an .aspx extension) and a "code-behind" file (identified by a .cs extension). We use the term code behind because this is how the relationship between the two files is specified in the hypertext markup language (HTML) file.
The following only includes the relevant portion of Start.aspx that shows server-side controls and field validation in action. (The complete source for this entire application can be found on this book's Web site.)
<asp:TextBox id=m_UserTB runat="server" ></asp:TextBox> <asp:TextBox id=m_ProjectUrlTB runat="server" ></asp:TextBox> <asp:Button id=m_OpenProjectButton runat="server" Text="Open Project"></asp:Button> <asp:requiredfieldvalidator id=m_UserValidator runat="server" controlToValidate="m_UserTB" errorMessage="Must enter your email address as your user name." /> <asp:requiredfieldvalidator id=m_ProjectFileValidator runat="server" controlToValidate="m_ProjectUrlTB" errorMessage="Must enter Baton project URL." /> <asp:validationsummary id=m_StartPageValidSummary runat="Server" headertext="Form errors exist:" />
This listing shows an example of the <asp:TextBox> and <asp:Button> server-side controls. The first text box control is for the user ID, and the second one is for the project URL. The id attribute specifies the name of the member variable that represents these controls in the code behind this form. Not surprisingly, the <asp:button> control is the Open Project button. Each of the text boxes has its own <asp: requiredfieldvalidator>, which is the field validation control. (See Chapter 2 for a full explanation of validation controls.)
Now we can discuss the code behind this form. The following is code from the Start.cs file, which was specified as this form's Codebehind attribute.
namespace ManagedSynergy { using System; using System.IO; using System.Collections; using System.ComponentModel; using System.Data; using System.Drawing; using System.Web; using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; using System.Runtime.Serialization.Formatters.Binary; public class Start : System.Web.UI.Page { protected System.Web.UI.WebControls.ValidationSummary m_StartPageValidSummary; protected System.Web.UI.WebControls.RequiredFieldValidator m_ProjectFileValidator; protected System.Web.UI.WebControls.RequiredFieldValidator m_UserValidator; protected System.Web.UI.WebControls.Label m_ExceptionMsg; protected System.Web.UI.WebControls.Button m_OpenProjectButton; protected System.Web.UI.WebControls.TextBox m_ProjectUrlTB; protected System.Web.UI.WebControls.TextBox m_UserTB; public Start() { Page.Init += new System.EventHandler(Page_Init); } protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { // // Evals true first time browser hits the page // } } protected void Page_Init(object sender, EventArgs e) { // // CODEGEN: This call is required by the ASP+ Windows Form Designer. // InitializeComponent(); } /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { m_OpenProjectButton.Click += new System.EventHandler (this.OpenProjectButton_Click); this.Load += new System.EventHandler (this.Page_Load); } public void OpenProjectButton_Click (object sender, System.EventArgs e) { // Save the user ID in the Application object for use later Page.Application.Contents["User"] = m_UserTB.Text; Page.Application.Contents["ProjectUrl"] = m_ProjectUrlTB.Text; // Create a baton object so we can access the InternetBaton // Web service ManagedSynergy.localhost.Baton Baton = new ManagedSynergy.localhost.Baton(); // A Baton query for the existence of the project specified in // the m_ProjectUrlTB field if (Baton.ProjectExists(m_UserTB.Text, Project.UrlToName(m_ProjectUrlTB.Text))) { try { // Create a project object that points to the shared // project file. Project Proj = new Project(); // Load data from the shared project file ProjectUrl Project.Load(ref Proj, m_UserTB.Text, m_ProjectUrlTB.Text); // Make project data available to other pages Page.Application.Contents["Project"] = Proj; // Point browser to project page Response.Redirect("ProjectPage.aspx"); } catch (System.Runtime.Serialization.SerializationException) { m_ExceptionMsg.Text = "Could not load project file. The Baton server may be down or the project file may be corrupt or an incompatible format."; } } else { // Project does not exist so let's see if the user wants to create // a new one Response.Redirect("ConfirmCreate.aspx?ProjectUrl=" + m_ProjectUrlTB.Text); } } } }
As with the previous presentation code, we will not take the time to explain every aspect of this listing. Instead we highlight those sections that show .NET in action, as well as those aspects that are necessary for understanding the rest of the application.
The first thing that you should note is the control member declarations:
protected System.Web.UI.WebControls.ValidationSummary m_StartPageValidSummary; protected System.Web.UI.WebControls.RequiredFieldValidator m_ProjectFileValidator; protected System.Web.UI.WebControls.RequiredFieldValidator m_UserValidator; protected System.Web.UI.WebControls.Label m_ExceptionMsg; protected System.Web.UI.WebControls.Button m_OpenProjectButton; protected System.Web.UI.WebControls.TextBox m_ProjectUrlTB; protected System.Web.UI.WebControls.TextBox m_UserTB;
Each of these corresponds to the IDs that were specified in the previous presentation code. If you are using Visual Studio.NET, then members are automatically added to your code-behind class.
If you skip down to the InitializeComponent() class, you can see how the event handling is specified in .NET. There is only one event in which we are interested in this form, so the code ends up looking like the listing that follows. The control m_OpenProjectButton has its event handler set in the OpenProjectButton_Click() method:
private void InitializeComponent() { m_OpenProjectButton.Click += new System.EventHandler (this.OpenProjectButton_Click); this.Load += new System.EventHandler (this.Page_Load); }
The last thing to discuss in this file is the OpenProjectButton_Click() method itself. Clicking the Open Project button on the start page invokes this method. The first thing this code does is save the user's ID and the project URL in the application object:
// Save the user ID in the Application object for use later Page.Application.Contents["User"] = m_UserTB.Text; Page.Application.Contents["ProjectUrl"] = m_ProjectUrlTB.Text;
The application object operates just as it did in ASP, and assigning it values makes the values available to other pages in the application. We use the application object rather than the session object because we need to get at these values in the Web service code, which has a different session instance. The code behind the various forms in this application also makes use of the values stored in the application object.
Next we use the InternetBaton Web service to determine whether this project already exists:
// A Baton query for the existence of the project specified in // the m_ProjectUrlTB field if (Baton.ProjectExists(m_UserTB.Text, Project.UrlToName(m_ProjectUrlTB.Text)))
If the project does not exist, we ask the users if they want to create a project. If the project does exist, then we load the project over the Internet using InternetBaton to get the most up-to-date project file:
// Load data from the shared project file ProjectUrl Project.Load(ref Proj, m_UserTB.Text, m_ProjectUrlTB.Text); // Make project data available to other pages Page.Application.Contents["Project"] = Proj; // Point browser to project page Response.Redirect("ProjectPage.aspx");
After loading the project, we again use the application object to save a reference to the project object, which now holds all the most current project data. Finally, we use the ASP .NET response object to point the browser to the main application page we showed previously.
While we are here, we can take a look at the code that loaded the project data from an InternetBaton resource accessed over the Internet. The Load() method is a member of the project class that is located in the Project.cs file. The project class and the other classes in this file encapsulate and abstract the notion of a ManagedSynergy project. This allows the code behind the forms in this application and the methods that are used as Web services to cleanly call project-related primitives. Without these classes, the implementation of a project would be spread around various places in the application, and the methods of these classes would have to be redundantly coded "in-line" every time the functionality was needed. With the encapsulated approach the code is centralized, so fixing an error only requires the user to make a change in one place rather than search the application looking for the various other places that might also need to be changed. As we discuss this implementation, keep in mind that architecturally the forms and the Web service exist for the purpose of connecting clients to the functionality encapsulated in the project classes.
Turning our attention to the Project.Load() method, you will notice that we used a static method for this code. We chose to use a static method because we had a chicken-or-egg type of problem; it seemed safer to pass in the object rather than assign the deserialized object to the this reference. That is, the alternative would have involved invoking the Load() method on an object instance, and inside that method we would have needed to point the this reference to the new object.
public static void Load(ref Project Project, string UserName, string ProjectUrl) { // Create a Web-based stream gives access to the project file // over the Internet WebRequest Request = WebRequest.Create("http://<IB>" + Project.UrlToName(ProjectUrl)); Stream ProjStream = Request.GetResponse().GetResponseStream(); try { // Create the formatter BinaryFormatter ProjFormatter = new BinaryFormatter(); // Load the project file from the Web-based stream. This always // causes the most current project file to be loaded. Project = (Project) ProjFormatter.Deserialize(ProjStream); } finally { ProjStream.Close(); } }
The WebRequest class shown here is provided by the .NET framework. This class can be used to treat a uniform resource identifier (URI) as a stream, which is exactly what the BinaryFormatter requires for deserialization. Once the stream is established, the binary formatter can be used to deserialize the Project object and assign the resulting value to the reference argument that was passed to the method. This code is a good example of the power and simplicity of the .NET framework.
Creating a New Project
When creating a new project, we assume that the project URL specified by the user points to a project file that does not yet exist. We want to ask users whether they want to create a new project. (Who knows? They may have mistyped the URL and do not intend to create a new project.) If the users decide to create a new project, their browser will point to the CreateProject.aspx page shown in Figure 4-3.
Figure 4-3 Create Project Page for ManagedSynergy.
Although we said previously that each page is so similar we only need to examine the startup page, there is one detail in this presentation code worth revisiting. This page makes use of the <asp:regularexpressionvalidator> regular expression field validator. The following is how this is defined in CreateProject.aspx:
<asp:regularexpressionvalidator id=m_DigitNumOfReviewersValidator runat="server" controlToValidate="m_NumOfReviewersTB" errorMessage="Number of reviewers must be a numeric value" display="none" validationexpression="[0-9]*"/>
The regular expression validator ensures that the characters entered in a field on a form match a pattern (i.e., the regular expression). We used this validation control to make sure that the value for the number of reviewers is a numeric value. We already discussed each of the attributes used in this definition, and the ones shown here have the same meaning. What is new here is the validationexpression attribute. This attribute defines the regular expression that should be used to validate the field. We specified the regular expression "[0-9]*", which says that the input must be zero or more digits and nothing else. Because you can set more than one validator on a given field, we were able to add an <asp:requiredfieldvalidator> for the same field to catch a case in which nothing at all is entered in the field (which would have been valid if we had used <asp:regularexpressionvalidator> only).
The following is the code behind this page. Many of the standard steps are repeated in this listing:
namespace ManagedSynergy { using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Drawing; using System.Web; using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; public class CreateProject : System.Web.UI.Page { protected System.Web.UI.WebControls.Label m_ExceptionMessage; protected System.Web.UI.WebControls.ValidationSummary m_CreateProjectValidSummary; protected System.Web.UI.WebControls.RegularExpressionValidator m_DigitNumOfReviewersValidator; protected System.Web.UI.WebControls.RequiredFieldValidator m_RequiredNumOfReviewersValidator; protected System.Web.UI.WebControls.RequiredFieldValidator m_AdminEmailValidator; protected System.Web.UI.WebControls.RequiredFieldValidator m_AdminNameValidator; protected System.Web.UI.WebControls.Button m_CreateButton; protected System.Web.UI.WebControls.TextBox m_NumOfReviewersTB; protected System.Web.UI.WebControls.TextBox m_AdminEmailTB; protected System.Web.UI.WebControls.TextBox m_AdminNameTB; protected System.Web.UI.WebControls.Label m_ProjectLabel; private static string s_ProjectUrl; public CreateProject() { Page.Init += new System.EventHandler(Page_Init); } protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { // Save this for later s_ProjectUrl = Page.Request.QueryString["ProjectUrl"]; // Set the form heading m_ProjectLabel.Text = Project.UrlToName(s_ProjectUrl); // Give default values to the fields m_AdminEmailTB.Text = (string)Page.Application.Contents["User"]; m_NumOfReviewersTB.Text = "0"; // Default to no approval } } protected void Page_Init(object sender, EventArgs e) { // // CODEGEN: This call is required by the ASP+ Windows Form // Designer. // InitializeComponent(); } /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { m_CreateButton.Click += new System.EventHandler (this.CreateButton_Click); this.Load += new System.EventHandler (this.Page_Load); } public void CreateButton_Click (object sender, System.EventArgs arg) { Project Proj; string UserID = (string)Page.Application.Contents["User"]; string ProjectUrl = (string)Page.Application.Contents["ProjectUrl"]; try { Proj = new Project(UserID, ProjectUrl, Page.MapPath("Projects"), m_AdminNameTB.Text, m_AdminEmailTB.Text, Convert.ToInt32(m_NumOfReviewersTB.Text)); // Store project in Application object for other pages to use Page.Application.Contents["Project"] = Proj; // Add the newly create project file to InternetBaton Proj.AddProjectToBaton(); // Point the browser to the project page Response.Redirect("ProjectPage.aspx"); } catch (Exception e) { m_ExceptionMessage.Text = "Error: \"" + e + "\\"; } } } }
There are a lot more server-side controls in this code than there were in the previous page but other than that there is nothing new. There is, however, something new in the implementation of the Page_Load() method. Previously, we did not have any work that needed to be done in this framework method, which gets invoked every time the page is loaded. The code in the following conditional, on the other hand, is only executed the first time the browser hits this page:
if (!IsPostBack) { // Save this for later s_ProjectUrl = Page.Request.QueryString["ProjectUrl"]; // Set the form heading m_ProjectLabel.Text = Project.UrlToName(s_ProjectUrl); // Give default values to the fields m_AdminEmailTB.Text = (string)Page.Application.Contents["User"]; m_NumOfReviewersTB.Text = "0"; // Default to no approval desired }
The first two lines of code set the label in the page's display heading, and the second two set default values for the form fields.
This brings us to CreateButton_Click(), which is the last method in this file and the method that is invoked when the Create button is clicked. The first two methods get the values that we saved in the application object when the user first logged in to the application. The remaining code in this method (the code inside the try block) creates a new project object by passing on its initial values, adds a link to the newly created project to InternetBaton, and finally, points the browser to the ProjectPage.aspx so that the user can start adding items. We discuss the exception handling itself when we take a look at adding items to the project.
As we did previously, let's take a look at another Project method before we move on. The purpose of the AddProjectToBaton() method is to add a link that points this newly created project file to InernetBaton.
When we thought about the design of the ManagedSynergy application, our focus was on adding some approval workflow to document revision. Using InternetBaton was a logical choice for sharing the user's document, and on top of that we would add the approval workflow. Then we had to figure out how to share the ManagedSynergy project file itself, which is when we came up with the idea that we could use an InternetBaton resource. This would solve the problem of managing the changes to the project file, as well as sharing the result.
Therefore in this implementation the project file itself is not a link that the user adds but instead is an internal link that the application manages behind the scenes. From the user's perspective, the project file somehow gets magically shared. In reality, there is no magic at all. It is yet another example of collaboration in action. Let's see how all this gets done.
public void AddProjectToBaton() { ManagedSynergy.localhost.Baton Baton = new ManagedSynergy.localhost.Baton(); // Save the project data to the shared directory Save(); // Add yourself to InternetBaton which will initially point // participants to this new added project Baton.Add(UserID, this.Name, this.Name); }
As you can see, there is not a lot of code required in this method either. First, the project data is saved to disk in the shared directory. Saving to the shared directory allows others to access it as InternetBaton redirects their Web request to this server. Once the file is safely saved to disk, the only thing left is to add a link to this newly created project file to InternetBaton.
The story here is not complete until we factor in the Project.Save() method.
public void Save() { // Create the directory if it does not already exist if (!Directory.Exists(m_ProjectPath)) Directory.CreateDirectory(m_ProjectPath); // Creates a project if one does not exist or updates its contents // if project does exist FileStream ProjectStream = (FileStream)File.Create(m_ProjectPath + "\\" + Name); // Create a serializer that will know how to persist BinaryFormatter Serializer = new BinaryFormatter(); // Persist the state of the project to the project file Serializer.Serialize(ProjectStream, this); // Free the file handle since there is no telling when, or even if, GC // will get around to it. ProjectStream.Close(); }
The Save() method starts by ensuring that the shared project directory exists. The code creates the specified directory and any missing directories in between the root and the specified directory:
Directory.CreateDirectory(m_ProjectPath);
This shared directory lives in the virtual path of the ManagedSynergy Web site so that it is accessible for the redirection of requests by InternetBaton. Next, a file stream is created so that it can be passed to the serializer. The serializer uses the meta information associated with the object itself to write out the state of the object in a way that allows it to be read back in. This serialization is the format of our project file. When these steps are complete, the only thing left to do is to close the file stream, which frees up the file handle. Notice that we need to explicitly close the stream rather than depend on it being freed when the object is destroyed. This task is typically carried out by the garbage collector, which is not even guaranteed to run during an application session.
At this point, we know how this application opens existing project files or creates new ones. Next, we examine how the user adds items to this project.
Adding a Project Item
When adding a project item, we assume that the user has already created a project and is ready to add an item to that project. The screen shot in Figure 4-4 shows the Add page. As you can see, the user specifies the display name for the new item, the URL of the shared document, and a brief description of the shared document. Jumping right to the code behind this form, we have the following:
Figure 4-4 Add Page for ManagedSynergy.
namespace ManagedSynergy { using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Drawing; using System.Web; using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; using System.Runtime.Serialization.Formatters.Binary; using System.IO; public class AddForm : System.Web.UI.Page { protected System.Web.UI.WebControls.RequiredFieldValidator m_UrlValidator; protected System.Web.UI.WebControls.RequiredFieldValidator m_NameValidator; protected System.Web.UI.WebControls.Label m_ExceptionMessage; protected System.Web.UI.WebControls.ValidationSummary m_AddItemValidSummary; protected System.Web.UI.WebControls.RequiredFieldValidator m_DescriptionValidator; protected System.Web.UI.WebControls.Button m_AddButton; protected System.Web.UI.WebControls.TextBox m_DescriptionTB; protected System.Web.UI.WebControls.TextBox m_UrlTB; protected System.Web.UI.WebControls.TextBox m_NameTB; protected System.Web.UI.WebControls.Label m_ProjectLabel; public AddForm() { Page.Init += new System.EventHandler(Page_Init); } protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { // Get the project data stored in the Application object Project Proj = (Project)Page.Application.Contents["Project"]; // Set the project name in the page heading m_ProjectLabel.Text = Proj.Name; } } protected void Page_Init(object sender, EventArgs e) { // // CODEGEN: This call is required by the ASP+ Windows Form // Designer. // InitializeComponent(); } /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { m_AddButton.Click += new System.EventHandler (this.AddButton_Click); this.Load += new System.EventHandler (this.Page_Load); } public void AddButton_Click (object sender, System.EventArgs arg) { // Retrieve project data that was saved in the Application object Project Proj = (Project)Page.Application.Contents["Project"]; try { // Add this new project item to the project Proj.AddItem(m_NameTB.Text, m_UrlTB.Text, m_DescriptionTB.Text); // Point browser to the project page Response.Redirect("ProjectPage.aspx"); } catch (System.Exception e) { string UserID = (string)Page.Application.Contents["User"]; string ProjectUrl = (string)Page.Application.Contents["ProjectUrl"]; // Re-load the project data since it may be out of sync Project.Load(ref Proj, UserID, ProjectUrl); // Display a message on the form informing user about error m_ExceptionMessage.Text = e.Message; } } } }
The only item we need to discuss in this listing is the AddButton_Click() method. Once the user fills out the form and then clicks the Add button, the AddButton_Click() method is invoked. The first thing that happens is that the project object is retrieved from the application object. The code inside the try block adds the new item to the project and then returns to the project page.
Looking once more in the Project.cs source file reveals the following code from the Project.AddItem() method:
public void AddItem(string ItemName, string ItemUrl, string Description) { ManagedSynergy.localhost.Baton Baton = new ManagedSynergy.localhost.Baton(); // Check out the project file so we can add an item CheckOut(); try { // Create a new item to add to project ProjectItem Item = new ProjectItem(this, ItemName, ItemUrl, Description, InitialApprovalStatus, "[Initial Check In]", 0); // Add item to Baton Baton.Add(UserID, this.Name, ItemName); // Add the item to project m_ProjectItems.Add(Item); // Save the project data to the project file (in effect updating it) Save(); // Check in the project file which will point the other users to // the project file we just modified. All in all, project file is // // not kept locked all that long. CheckIn(); } catch { // Since we were unsuccessful in our attempt to take this action // we need to undo the check out we did for the project UndoCheckOut(); // Re-throw so that others can perform exception handling throw new Exception("Could not add item to \"" + m_ProjectPath + Name + "\". The disk may be full or ..."); } }
After creating the InternetBaton object, this code calls the Project.CheckOut() method, which checks out the project file. This step is necessary because adding an item to the project changes the state of the project file, requiring us to first check it out. We then create a new item based on the values passed to the AddItem() method. After creating the new item, we add it to InternetBaton using the Web service method Baton.Add(). We add the newly created object to the m_ProjectItems array, save the project file to its shared location, and then check the project file back in using the Project.CheckIn() method.
We need to mention a couple of things. First, we chose the ArrayList as the type for the m_ProjectItems member of the project class because it acts like an array but has a dynamic quality that allows it to grow as new elements are added. Therefore we got the best qualities of an array without the usual drawback of worrying about whether the array is big enough.
Second, we need to discuss exception handling. The first thing we do is check out the project file, and the last thing we do is check it back in. What if something happens before we are able to check the project file back in? For example, if the disk were full and the Save() method caused an exception, we would skip checking in the project and the other participants would not be able to access the project file because it would still be locked. The try/catch block is used to solve this dilemma. If anything causes an exception after the point at which the project file is checked out, then the catch block will catch the exception and undo the check-out. The catch block also rethrows the exception with an associated specialized message. If we do not rethrow the exception then our catch handler would have totally handled it, and the caller would be unaware of the problem. In this application, the exception rethrow is caught by the presentation code, and a proper message (based on the error message we set) can be shown to the user. Take a look at the AddButton_Click() method in the previous listing to examine the catch handler that would catch the rethrow. This same exception handling pattern is also used in several other project methods.
Deleting a Project Item
When deleting a project item, we assume that the user has already created a project, added items to the project, and now wants to delete an item from the project. To delete an item from a project, the user must select it from the project items list box and select "Delete" on the action bar.
The code for deleting an item is so similar to the code used for adding an item that it would be of little value to repeat here. The only real difference is that the code deletes an item instead of adding it. You can find the source to both AddPage.cs and DeletePage.cs on this book's Web site if you would like to compare these files for yourself.
Checking out an Item
When checking out an item, we assume that the user has already created a project, added items to the project, and now wants to check out an item so that it can be modified. To check an item out, users must first select it from the project items list box and then select "Check Out" on the action bar. After users confirm that they want to check the item out, the ProjectItem.CheckOut() method is called:
public void CheckOut() { ManagedSynergy.localhost.Baton Baton = new ManagedSynergy.localhost.Baton(); // We need to check out the project file before we change its // state by checking out a project item m_Project.CheckOut(); try { // Make this item reflect a checked out state ReflectCheckedOutState(); // Save the project data to the project file (in effect updating it) m_Project.Save(); // Check out the project item Baton.CheckOut(m_Project.UserID, m_Project.Name, this.Name); // Check in the project file m_Project.CheckIn(); } catch { // Since we were unsuccessful in our attempt to take this action // we need to undo the check out we did for the project m_Project.UndoCheckOut(); // Re-throw so that others can perform exception handling throw new Exception("Could not check item out of \"" + m_Project.ProjectPath + Name + "\". The disk may be full or the project file has be deleted."); } }
Keep in mind that this method is part of the ProjectItem class, and in a sense it is the item attempting to check itself out. Once again, the code begins with the obligatory checking out of the project file. Checking out a file changes the state of a project item to reflect the fact that it is being revised and does not yet have a check-in comment. These changes are encapsulated in the ProjectItem.ReflectCheckedOutState() method:
private void ReflectCheckedOutState() { // Checking out a file always puts it in the "in Revision" state m_AStatus = ApprovalStatus.InRevision; // Reset check in comments and set it to "To Be Determined" m_CheckInComment = "[TBD]"; }
Once the state has been updated, the project object is saved to disk, after which the check-out request is made to InternetBaton. If all this goes well, the last thing to do is check the project file back in.
During the design of this method, we had to consider the possibility that the desired file may have already been checked out by someone else. If it has, the check-out fails and causes an exception that takes us back to the check-out form, where the error is reported. The problem is that the state of the project will have already been written out to disk. Our solution to this dilemma was to have the exception handler that reports the error also roll back the project file by reloading it from InternetBaton using deserialization over the Internet. The code in the ConfirmCheckOut.aspx file looks like the following:
public void OKButton_Click (object sender, System.EventArgs arg) { // Retrieve project that was saved in the Application object Project Proj = (Project)Page.Application.Contents["Project"]; // Get the project item that corresponds to index pass to this page ProjectItem Item = (ProjectItem)Proj.ProjectItems[s_ItemIndex]; try { // Check out the specified item Item.CheckOut(); // Point browser to the project page Response.Redirect("ProjectPage.aspx"); } catch (System.Exception e) { string UserID = (string)Page.Application.Contents["User"]; string ProjectUrl = (string)Page.Application.Contents["ProjectUrl"]; // Re-load the project data since it may have become out of sync Project.Load(ref Proj, UserID, ProjectUrl); m_ExceptionMessage.Text = e.Message; } }
As you can see, the catch block first reloads the project object and then displays an error message that explains what happened.
Viewing a Project Item
When viewing a project item, we assume that the user has already created a project, added items to the project, and now wants to view an item's contents. To view an item from a project, the user must select it from the project items list box and then select "View" on the action bar. After a user requests to view an item, the browser points to the URL associated with the item.
When we thought about how a user could download documents to work on them, we decided that it could be accomplished by selecting the Save As option in the browser. Having first checked out an item, the user could view the item and then choose the Save As option to save the document to a personal work directory. The idea is that this file in the user's work directory is the one that would later be checked back in. As we find out in the next scenario, the check-in process causes the file to be copied to the shared project directory and makes the changes public when InternetBaton is notified of the check-in.
The following is the code behind the "View" callback:
public void ViewButton_Click (object sender, System.EventArgs e) { if (m_ProjectListBox.SelectedIndex >= 0) { // Get the project data and the selected project item Project Proj = (Project)Page.Application.Contents["Project"]; ProjectItem ProjItem = (ProjectItem)Proj.ProjectItems[m_ProjectListBox.SelectedIndex]; // Point browser to the shared project item. This is not to be // confused with a local version of the same file, if one even // exists. This will view the most current version of this // file on whichever machine is currently holding it. Response.Redirect(ProjItem.ItemUrl); } else // Complain to user m_DetailCWC.Text = "You must select a project item before you try to view it"; }
This code begins by ensuring that an item has been selected before retrieving the project data. Using the index from the list (which corresponds to the index of the correct item in the project's item array), the selected item is retrieved. At this point, we simply use the ItemUrl property to point the browser to the item in question. We merely pass it as the argument to the Response.Redirect() method.
Checking in an Item
When checking in an item, we assume that the user has previously checked out a project item and now wants to check that item back in. To check in an item, the user first selects it from the project items list box and then selects "Check In" on the action bar. The screen shot in Figure 4-5 shows the check-in page. To check an item in, the user needs to specify the local file that contains the author's changes and enter a brief comment explaining the changes that were made. The file being checked back in is the same one that was downloaded using the Save As option in the check-out scenario and has presumably been modified in some way. When a file is checked back in, one of the behindthe-scenes activities involves publishing the new version of the document by copying it to a standard shared location that is a subdirectory in the application's virtual root.
Figure 4-5 Check-In Page for ManagedSynergy.
The code behind this page fits the same pattern we covered previously when discussing similar types of requests. Once the user selects "Check In" from the action bar, the ProjectItem.CheckIn() method is called:
// Checks in project item public void CheckIn(string LocalPath, string CheckInComment) { ManagedSynergy.localhost.Baton Baton = new ManagedSynergy.localhost.Baton(); // We need to check out the project file before we change its // state by checking in a project item m_Project.CheckOut(); try { // Make this item reflect a checked in state ReflectCheckedInState(CheckInComment); // Save the project data to the project file (in effect updating it) m_Project.Save(); // Publish the local item in the project directory in its shared // location Publish(LocalPath); // Check in the project item Baton.CheckIn(m_Project.UserID, m_Project.Name, this.Name); // Check in the project file m_Project.CheckIn(); } catch { // Since we were unsuccessful in our attempt to take this action // we need to undo the check out we did for the project m_Project.UndoCheckOut(); // Re-throw so that others can perform exception handling throw new Exception("Could not check item in to \"" + m_Project.ProjectPath + Name + "\". The disk may be full or the project file has been deleted or is read-only."); } }
Two aspects of this method that need to be discussed are ReflectCheckedInState() and Publish(). Checking in a file changes the state of a project item in various ways. These changes are encapsulated in the ProjectItem.ReflectCheckedInState() method, which contains the following lines of code:
private void ReflectCheckedInState(string CheckInComment) { // Set the initial approval status for items in this project m_AStatus = m_Project.InitialApprovalStatus; // Reset the number of reviews for this change m_NumberOfReviews = 0; // Record the check in comments that the author made m_CheckInComment = CheckInComment; // Get rid of reviews from last document revision and start freah // with this new change m_ReviewItems.Clear(); }
Checking in an item requires that the approval status be reset to an initial approval status. This initial approval status is different depending on whether there are reviewers involved. For example, if there are no reviewers, then the status is automatically "Approved." On the other hand, if there are reviewers involved, then the status is "In Review." All of this is taken care of in the ProjectItem.InitialApprovalStatus property. This is an example in which a property is not actually the value of a member variable but instead is a calculated value. You would never know it by looking at the line of code that sets the status though.
The other steps that need to be followed to properly set the state on a checked-in item are resetting the number of reviews to zero, assigning the check-in comment, and clearing out all the old reviews.
We mentioned that publishing a change to a project item requires it to be copied to its shared directory. The following code shows how the ProjectItem.Publish() method accomplishes this task:
private void Publish(string LocalItemPath) { try { // If necessary, create the shared directory to hold project files if (!Directory.Exists(m_Project.ProjectItemsPath)) Directory.CreateDirectory(m_Project.ProjectItemsPath); // Copy file to its shared location File.Copy(LocalItemPath, m_Project.ProjectItemsPath + "\\" + Name, true); } catch { // Specialize the exception and re-throw it throw new Exception("\"" + LocalItemPath + "\" could not be published to the following shared location: \"" + m_Project.ProjectItemsPath + "\". Make sure the file path you specified above is correct and the disk is not full and that you have the correct permissions."); } }
This code uses the project name to create a directory, if one does not exist, in the shared directory (which is in the virtual root of this application). This shared directory was revealed to InternetBaton when we first added the item. The static File.Copy() method is used to actually copy the file.
Reviewing an Item
When reviewing an item, we assume that the user has already created a project, added items to that project, and made changes to an itemand now a reviewer wants to submit a review of those changes. To submit a review, the user must first select the item from the project items list box and then select "Review" on the action bar. The screen shot in Figure 4-6 shows the review page. Reviewing an item involves entering a brief review that is based on the reviewer's assessment of the most recent changes. The reviewer specifies a good or bad verdict using the dropdown control on the form.
Figure 4-6 Review Page for ManagedSynergy.
The code behind this page is found in the ReviewPage.aspx file. It is so similar to patterns we have already discussed that reviewing this code would not broaden your understanding of applied .NET. Therefore we have included the code without any discussion.
When the user clicks the OK button, the following code is invoked:
public void OKButton_Click (object sender, System.EventArgs arg) { // Get the user name that was stored in the Application object string User = (string)Page.Application.Contents["User"]; ApprovalStatus Verdict; // Get the project data that was stored in the Application object Project Proj = (Project)Page.Application.Contents["Project"]; try { // Record the verdict of the review if (m_VerdictDDL.SelectedItem.Text == EnumToString.AStatusToString(ApprovalStatus.Approved)) Verdict = ApprovalStatus.Approved; else Verdict = ApprovalStatus.Revise; ReviewItem Review = new ReviewItem(User, m_ReviewTB.Text, Verdict); // Add the review to the project s_ProjectItem.AddReview(Review); // Point your browser to the project page Response.Redirect("ProjectPage.aspx"); } catch (System.Exception e) { string UserID = (string)Page.Application.Contents["User"]; string ProjectUrl = (string)Page.Application.Contents["ProjectUrl"]; // Re-load the project data since it may have become out of sync Project.Load(ref Proj, UserID, ProjectUrl); // Display a message on the form that informs the user about error m_ExceptionMessage.Text = e.Message; } }
The code that submits the review looks like the following:
public void AddReview(ReviewItem Review) { // We need to check out the project file before we change its // state by adding a review for this item m_Project.CheckOut(); try { // Add review to the review item object array m_ReviewItems.Add(Review); // Account for addition of review m_NumberOfReviews++; // Determine the approval status of this item. DetermineApprovalStatus(); // Save the project data to the project file (in effect updating it) m_Project.Save(); // Check in the project file m_Project.CheckIn(); } catch { // Since we were unsuccessful in our attempt to take this action // we need to undo the check out we did for the project m_Project.UndoCheckOut(); // Re-throw so that others can perform exception handling throw new Exception("Could not add a review ... "); } }
Viewing an Item's Properties
When viewing an item's properties, we assume that the user has already created a project, added items to that project, and now wants to see the various properties for an item. To view an item's properties, the user must first select it from the project items list box and then select "Properties" on the action bar. The screen shot in Figure 4-7 shows the properties page. The properties for an item are shown to the right of the project items list box.
Figure 4-7 Properties Page for ManagedSynergy.
When implementing this function, we decided that a custom control would work quite nicely for displaying an item's properties. We needed the ability to dynamically create HTML so that each request to display an item's properties could be uniquely handled. This was especially important because the number of reviews is arbitrary. The code behind this form is as follows:
public void PropertiesButton_Click (object sender, System.EventArgs e) { ManagedSynergy.localhost.Baton Baton = new ManagedSynergy.localhost.Baton(); string DetailFormat = "<table><tr><td><b style=\"FONT-SIZE: 14pt; FONT-FAMILY: 'Arial Narrow'\">Name:</b></td></tr><tr><td>{0}</td></tr><tr><td><b style=\"FONT-SIZE: 14pt; FONT- FAMILY: 'Arial Narrow'\">URL:</b></td></tr><tr><td>{1}</td></tr><tr><td><b style=\"FONT-SIZE: 14pt; FONT-FAMILY: 'Arial Narrow'\">Description:</b></td></tr><tr><td>{2}</td></tr><tr><td><b style=\"FONT-SIZE: 14pt; FONT-FAMILY: 'Arial Narrow'\">Revision Status:</b></td></tr><tr><td>{3}</td></tr><tr><td><b style=\"FONT-SIZE: 14pt; FONT-FAMILY: 'Arial Narrow'\">Revision Comment:</b></td></tr><tr><td>{4}</td></tr><tr><td><b style=\"FONT- SIZE: 14pt; FONT-FAMILY: 'Arial Narrow'\">Approval Status:</b></td></tr><tr><td>{5}</td></tr><tr><td><b style=\"FONT-SIZE: 14pt; FONT-FAMILY: 'Arial Narrow'\">Reviews:</b></td></tr></table>"; string Detail; if (m_ProjectListBox.SelectedIndex >= 0) { // Get the user name that was stored in the Application object string User = (string)Page.Application.Contents["User"]; // Get the project data and the selected project item Project Proj = (Project)Page.Application.Contents["Project"]; ProjectItem ProjItem = (ProjectItem)Proj.ProjectItems[m_ProjectListBox.SelectedIndex]; RevisionStatus RStatus = (Baton.IsCheckedIn(User, Proj.Name, ProjItem.Name)) ? RevisionStatus.CheckedIn : RevisionStatus.CheckedOut; // Load up the objects that will provide the values for the format string object[] FormatArgs = {ProjItem.Name,ProjItem.ItemUrl,ProjItem.Description,EnumToString.RStatusToString(RStatus),Pro jItem.CheckInComment,EnumToString.AStatusToString(ProjItem.AStatus)}; // Create the dynamic property details for this item Detail = String.Format(DetailFormat, FormatArgs); // Add the item reviews to the end of the details string for each (ReviewItem R in ProjItem.ReviewItems) Detail += "<b>" + R.Reviewer + "'s</b> verdict was <b>" + EnumToString.AStatusToString(R.Verdict) + "</b> with this explaination:<br>" + R.Review + "<br><br>"; // Assign the property details to our custom control so it can // be rendered. m_DetailCWC.Text = Detail; } else // Complain to user m_DetailCWC.Text = "You must select a project ... "; }
This code uses string formatting to fill in the placeholders that were placed in the properties HTML. The placeholders are the {N} elements found in the HTML, where N is the number corresponding to the formatting argument that is to be substituted (much like a printf() statement in C or C++). Once the placeholders have been filled in, then the reviews are appended to the HTML string we are building by looping through all the reviews and adding them one at a time. Finally, we take the resulting HTML string and assign it to the Text property of the custom control we created.
The following is the complete implementation of the custom control:
[DefaultProperty("Text"), ShowInToolbox(true), ToolboxData("<{0}:ItemDetail runat=server></{0}:ItemDetail>")] public class ItemDetail : System.Web.UI.WebControls.WebControl { private string text; [Bindable(true), Category("Appearance"), DefaultValue(""), Persistable(PersistableSupport.Declarative)] public string Text { get { return text; } set { text = value; } } protected override void Render(HtmlTextWriter output) { output.Write(Text); } }
That is all there is to implementing a custom control. We added the Text property to make it easy for clients to associate the HTML they want to be displayed as the value of this control, but the only real requirement is that you have a Render() method with the right signature. This method is called, and it does whatever it needs to with the HtmlTextWriter argument. In our case, we just wrote out the Text property we were given.
The other aspect to this custom control is how to use it in HTML. The following is a snippet from the PropertyPage.aspx file.
<%@ Register TagPrefix="AC4" NameSpace="ManagedSynergy" Assembly="ManagedSynergy" %> [Other unrelated HTML omitted...] <AC4:ITEMDETAIL id=m_DetailCWC runat="server"></AC4:ITEMDETAIL>
As you can see, the formatting for custom control is exactly like the formatting for any standard server-side control. You specify a tag prefix followed by the class name and an ID.
Invoking Administration Services
When invoking administration services, we assume that the user has already created a project and wants to perform some InternetBaton administration activities, such as adding a user to the project. The user merely selects "Admin" on the action bar, which causes the browser to point to the InternetBaton application. The code for this task is simple:
public void AdminButton_Click (object sender, System.EventArgs e) { // Point browser to the Baton Web site for any admin tasks // like adding users, creating projects, etc. Response.Redirect("http://www.InternetBaton.com/AdminPage.aspx"); }
Dynamic Status Updates and Overnight Project Replication
As we mentioned previously, ManagedSynergy exposes Web services in addition to its application functionality. These Web services allow the state of the project to be dynamically updated, as well as allow off-hours project synchronization. As was mentioned in Chapter 2 regarding dynamic status updates, InternetBaton calls on the Web service method VersionChanged(); it is up to the Web service to reconcile itself with the new state of the project. For off-hours project synchronization, InternetBaton calls the Web service method DownloadProjectItems(), and it is up to the Web service to download each of the items in the project. Following is a look at the VersionChanged() Web service method:
[WebMethod] public void VersionChanged(string ProjectID, string BatonID) { string UserID = (string)Application.Contents["User"]; Project Proj = new Project(); // Reload project object since something has changed Project.Load(ref Proj, UserID, Proj.ProjectUrl); // Overwrite old copy with updated project object Application.Contents["Project"] = Proj; }
Because Web services were introduced in Chapter 2, we do not elaborate on them here other than to follow up on a Chapter 3 topic. In that chapter, we discussed the fact that the C# language supports the concept of attributes as a means to express declarative information. The [WebMethod] attribute is a good example of how beneficial attributes can be. This attribute makes it possible to create the right kind of "plumbing" to properly use the method as a Web service. Chapter 2 also explained another use of attributes when it demonstrated how easy it is to implement asynchronous Web service calls with the addition of the [SoapMethod( OneWay = true )] attribute. Attributes will surely play a significant role in .NET development as a declarative way of having significant service provided for you.
The code for the VersionChanged() method leverages an application primitive we have already discussedthe Project.Load() method. Using the Load() method in this context causes the current state of the project file to be loaded into a project object, which then overwrites the outdated object.
The DownloadProjectItems() Web service method is merely a pass-through to Project.DownloadProjectItems(), the method that does the real work.
[WebMethod] public void DownloadProjectItems(string ProjectID, string EmailUserID) { // Download entire project Project.DownloadProectItems(ProjectID, EmailUserID); }
The Project.DownloadProjectItems() looks like this:
public static void DownloadProjectItems(string ProjectID, string EmailUserID) { Project Proj = new Project(); Project.Load(ref Proj, EmailUserID, ProjectID); foreach(ProjectItem Item in Proj.ProjectItems) Item.Download(); }
Once again we see another method making use of Project.Load() to initialize a project object with the most current project state. Previously we mentioned that one of the benefits of the approach we took with the project class was that the methods could be cleanly called; the equivalent functionality was not redundantly coded "in-line." This was clearly the case with the Load() method, which confirms the wisdom of our design choice.
Let's get back to the DownloadProjectItems() method. Once the project is loaded, the code loops through each of the project items and asks each one to download itself by calling the ProjectItem.Download() method that follows:
public void Download() { WebRequest Request; FileStream LocalFile = null; Stream ItemStream = null; int BufferSize = 1024; Byte[] Buffer = new Byte[BufferSize]; int BytesRead; try { // Request HTTP access to project item Request = WebRequest.Create(m_ItemUrl); // If necessary, create the shared directory to hold project files if (!Directory.Exists(m_Project.ProjectItemsPath)) Directory.CreateDirectory(m_Project.ProjectItemsPath); // Get the Web-based stream so we can download the file ItemStream = Request.GetResponse().GetResponseStream(); // Create local file that will hold downloaded project item LocalFile = new FileStream(m_Project.ProjectItemsPath + "\\" + m_Name, FileMode.Create, FileAccess.Write); // Read from download file and write to local file until copied BytesRead = ItemStream.Read(Buffer, 0, BufferSize); while (BytesRead != 0) { LocalFile.Write(Buffer,0,BytesRead); BytesRead = ItemStream.Read(Buffer, 0, BufferSize); } } finally { // Close these streams no matter how the thread of control // leaves this method if (ItemStream != null) ItemStream.Close(); if (LocalFile != null) { // Flush any remaining data to disk LocalFile.Flush(); LocalFile.Close(); } } }
This code is similar to the code used to load the project over the Internet. We use the WebRequest class to create a stream that can be used to access the contents of project items. After creating a directory to hold the downloaded items, a File stream is used to open a local file in write mode so that the contents of the project item can be copied. To copy the project item, the code simply reads from the downloaded stream and writes to the local stream until the entire file has been copied. We have used the finally construct because the file handles used by these streams need to be freed, no matter how this method is exited. Even if the code in the try block throws an exception, the code in the finally block is still executed.