Web Forms Interface

We'll create our Web Forms interface within the existing ProjectTracker solution, and you'll soon see that most of the steps necessary to set up a Web Forms application are very similar to those we made in Chapter 8 when we set up our Windows Forms application. In the ProjectTracker solution, choose File image from book Add Project image from book New Project. Make it an ASP.NET web application, and name it PTWeb as shown in Figure 9-6.

image from book
Figure 9-6: Adding the PTWeb project

As with our Windows Forms application, we need to reference the ProjectTracker.Library assembly and the CSLA .NET Framework assemblies by using the Add Reference dialog box. As shown in Figure 9-7, add the former via the Projects tab, and use the Browse button to navigate to the ProjectTracker.Library project's bin directory and select the CSLA .NET assemblies from there.

image from book
Figure 9-7: Referencing the CSLA and ProjectTracker.Library assemblies

Lastly, remove WebForm1.aspx from the project ”we'll add our own forms as we need them ”and right-click the project in Solution Explorer to set it as the startup project. This way, we can easily use the debugger to test our work as we go along.

Application Configuration

In our Windows Forms application, we added an App.config file and used it to provide configuration data for our client application. We need to do the same thing in the Web.config file of our Web Forms client application.

As we've discussed previously, the optimal configuration for a web application is to run the DataPortal server and the data-access code in the same process as our Web Forms application. The whole reason for using a remote DataPortal is so that we can get all our data-access code on a server and exploit database connection pooling. In a web environment, we're already on a server, so we can get database connection pooling with no need to go across yet another network boundary.

Tip  

Sometimes performance is trumped by security requirements, which mandate that the web server must talk to an application server behind a second firewall to do any data access. In that case, we know that we'll take a performance hit of about 50 percent when compared to running the data-access code directly on the web server. The upside is that we're potentially increasing our security.

In this chapter, we'll implement the optimal 2- tier configuration, with the server-side DataPortal running in the same process as our Web Forms UI. We'll also discuss how to configure the application to use a remote DataPortal server so that you can easily adjust if your security requirements dictate that approach.

The Web.config File

When we create a web application project, a Web.config file is created for us automatically. Open that file, and add an <appSettings> section to it, as shown here:

 <?xml version="1.0" encoding="utf-8" ?> <configuration>  <appSettings>     <add key="Authentication" value="CSLA" />     <add key="DB:Security"       value="data source=  server  ;initial catalog=Security;       integrated security=SSPI" />     <add key="DB:PTracker"       value="data source=  server  ;initial catalog=PTracker;       integrated security=SSPI" /> </appSettings>  <system.web> ... 
Tip  

Notice that <appSettings> is inside the <configuration> element, but not inside the <system.web> element.

By specifying the database connection strings here instead of supplying the location of the remote DataPortal , we're configuring the CSLA .NET Framework to run the DataPortal server code in our local process. Since it's the DataPortal server that invokes our business objects to run their data-access code, this means that our data-access code will also run in process.

This provides optimal performance because the data is retrieved from the database directly into our process, and is immediately available to our UI code. No object serialization or network data transfers occur. Also, since all our users are sharing the web server, all of the data-access code will run on this server. Since we've configured the connection strings to use integrated security, and our website allows anonymous users, all connections will be made under the ASP.NET user account. This means that we'll get database connection pooling.

Tip  

Typically, I use an application-specific username and password rather than using integrated security. Having all applications on the web server use the ASP.NET account means that all applications would have access to each other's data, which is often unacceptable. By using a specific user account and password, we restrict the data each application can access.

We're configuring the client to use CSLA .NET security, which is appropriate if we're allowing anonymous access to our website. If we want to use Windows' integrated security, we should change the configuration file shown here, and also change the IIS configuration to disallow anonymous access, thereby forcing users to authenticate using NT security credentials. We'll discuss security further when we implement our login form as part of the user interface.

A Note About Enterprise Services

With these configuration settings, our server-side DataPortal code will run in the ASP.NET process along with our Web Forms UI code and our business objects. This includes not only Server.DataPortal , but also Server.ServicedDataPortal.DataPortal .

The reason why ServicedDataPortal will also run in process is because it's configured to run in COM+ within a library application, and code in a library application runs within the host process that calls the components . This is ideal because we get the transactional protection of COM+ transactions at minimal overhead, since the code is running in process.

It's important to remember that the ASP.NET account can't register CSLA.Server.ServicedDataPortal.dll with COM+ because it doesn't have enough security rights. We must use the regsvcs.exe utility to register the DLL in COM+ from an administrator account, as we discussed in Chapter 5.

Using a Separate DataPortal Server

Sometimes, due to security requirements, we can't run data-access code directly on the web server. In that case, we need to configure the Web Forms client in the same way that we configured the Windows Forms client in Chapter 8.

Tip  

This section is for informational purposes only. Don't make the following changes to the Web.config file.

Instead of specifying the database-connection strings in Web.config , we need to specify where to find the remote DataPortal objects, as follows :

 <?xml version="1.0" encoding="utf-8" ?> <configuration>  <appSettings>     <add key="Authentication" value="CSLA" />     <add key="PortalServer"          value="http://  server  /DataPortal/DataPortal.rem" />     <add key="ServicedPortalServer"          value="http://server/DataPortal/ServicedDataPortal.rem" />   </appSettings>  <system.web> 

Obviously, you should change server to your application server's name or address. You may also specify a different port, since these application servers are often configured to listen on a nonstandard port, as shown here:

 http://  server  :8123/DataPortal/DataPortal.rem 

Then we need to configure the DataPortal server as described in Chapter 8. We need to make sure that the DataPortal host has our ProjectTracker.Library assembly in its bin directory.

Note  

If we update ProjectTracker.Library.dll , we need to update both the DataPortal server directory and the web UI directory at the same time.

As we discussed in Chapter 8, the DataPortal server needs the database connection strings and the ProjectTracker.Library assembly in order to provide appropriate data-access services to our client code. By making these simple changes to the Web Forms client's Web.config file and configuring a DataPortal host on another server, we switch our application from running everything in process on the web server, to running the data-access code on a separate server.

Again we'll stick with the higher-performance approach in this chapter, and run the server-side DataPortal in process with the UI and business objects in ASP.NET.

UI Overview

The Web Forms interface we'll create in this chapter is straightforward ”we'll focus on how to use business objects from Web Forms more than on how to create fancy Web Forms displays. By and large, the UI mirrors what we did in Chapter 8 with the Windows Forms UI, so we'll have the following two primary edit forms:

  • ProjectEdit

  • ResourceEdit

And the following two list forms for which the user can select a project or resource to edit or delete:

  • ProjectList

  • ResourceList

A resource can be assigned to a project, or a project can be assigned to a resource (depending on whether we're editing a project or a resource to start with). To accommodate this, we'll have two assignment forms, as follows:

  • AssignResource

  • AssignToProject

We also need a way to change the role a resource plays on a project. To do this, we'll create another form, as follows:

  • ChooseRole

Since we're configured to use CSLA table-based security, we need to prompt the user for a username and password. To ensure that they provide this information, we'll use ASP.NET forms-based security, but we have to create the form to get the user's credentials, as shown here:

  • Login

And finally, we need a main menu page. The following is the default page where the user starts interacting with our application:

  • Default

Let's start with the last item in this list and create the default menu page.

Default Form

Websites typically have a default page that's accessed when the user navigates to our virtual directory in their browser. This default page often provides menu or navigation support so that the user can easily access the functions provided by our application. Add a new web form, by using the Project image from book Add Web Form menu option, and name it Default . IIS is configured to look for a file named Default.aspx as the default page for a Web Forms application. Set the page properties as shown in Table 9-3.

Table 9-3: Properties for the default.aspx Page

Property

Value

pageLayout

FlowLayout

enableViewState

false

By setting the pageLayout property to FlowLayout , we allow the controls on the page to position themselves automatically as we place them on the form. Also, since this form has no data-entry fields, we have no need to maintain page state, so we set enableViewState to false . This minimizes the amount of data sent to and from the browser.

Add text and controls as shown in Figure 9-8.

image from book
Figure 9-8: Layout of the Default.aspx page

The controls are listed in Table 9-4.

Table 9-4: Controls on the default.aspx Page

Control

Properties

Label

ID=lblName ; Text=Use r

HyperLink

Text=Work with Projects ; NavigateUrl=Projects.aspx

HyperLink

Text=Work with Resources ; NavigateUrl=Resources.aspx

The only code we'll add to this page is in the page's Load event handler, where we'll set the value of lblName by using the user's identity, as follows:

 private void Page_Load(object sender, System.EventArgs e)     {  lblName.Text = User.Identity.Name;  } 

Notice that this isn't the same code that we used in the Windows Forms interface to retrieve the user's name for display in our StatusBar control. The ASP.NET environment provides its own principal object, separate from the thread's principal object. Any code within a Web Form can make use of the User property of the current page (or the HttpContext object) to get at the principal object. It's standard ASP.NET security code to retrieve the identity of the currently logged-in user, and it works whether the user was authenticated using CSLA .NET security or Windows' integrated security.

When we implement our CSLA .NET security, we'll make sure that both the current thread and the HttpContext have the correct principal object. This way, our web code can use the User property on each page, and our business objects can use the principal from the current thread.

Tip  

In .NET 1.1 this behavior was changed so the HttpContext principal and current thread principal are automatically kept in sync. This is better because it ensures that we have a consistent security context regardless of whether we use ASP.NET or pure .NET Framework code to access the current principal object.

In any case, everything we've just talked about implies that the user was authenticated prior to reaching our default page (or indeed any other page in the application). Let's implement that next .

Login Form and Security Configuration

ASP.NET supports the concept of forms-based security, which means that users will be automatically redirected to a login form before being allowed to access any other pages in our application. Better still, this is relatively easy to implement, especially since we already have a BusinessPrincipal class that does the actual work of authenticating the user.

Tip  

We wouldn't implement a login form when using Windows' integrated security. In that case, anonymous access to our site is disallowed by IIS, so IIS itself ensures that the user provides Windows credentials before it allows the user to access any of our application's forms.

Changing the Web.config File

Before we create the login form, we should update Web.config to turn on forms-based security. It contains an <authentication> element that you should change, as shown here:

  <authentication mode="Forms">     <forms name="login"         loginUrl="login.aspx" protection="All" timeout="60" />   </authentication>  

We're indicating that the authentication mode is Forms , which turns on forms-based security. Then we provide the name of our login form so that ASP.NET knows where to redirect any unauthenticated users who attempt to access our pages.

The protection attribute indicates that we're protecting all of the content in our site, and the timeout attribute indicates that the user's authentication remains valid for up to 60 minutes after her last access to the site.

Tip  

ASP.NET security is flexible and has many options that we're not addressing here. Please refer to Alex Homer and Dave Sussman's Professional ASP.NET (Wrox) for more detailed information about ASP.NET security.

If we wanted to use Windows' integrated security instead of CSLA .NET security, we'd set the authentication mode here to Windows , and also alter the configuration in <appSettings> , and change IIS to disallow anonymous access to our site. This will cause IIS to authenticate the user, and our thread's principal and identity objects will be WindowsPrincipal and WindowsIdentity objects.

We also need to update the <authorization> element in Web.config , as shown here:

 <authorization>  <deny users="?" /> <!-- Deny unauthorized users -->  </authorization> 

By default, all users are allowed to use our pages, but we want to deny all users access unless they're authenticated. This change ensures that only users who have been authenticated through our login form will be able to access the pages in our application.

Login Form

When using forms-based security, we need to implement a login form. All users are redirected here by ASP.NET to be authenticated. If we authenticate them successfully, then they can continue to use our site. If not, they continue to be redirected to this page.

Add a new web form to the project, naming it Login . Change the form's properties as shown in Table 9-5.

Table 9-5: Properties for the login.aspx Page

Property

Value

pageLayout

FlowLayout

With its elements added, the form should look similar to Figure 9-9.

image from book
Figure 9-9: Layout of the login.aspx page

The title is simple text, though the input controls are in a standard HTML table so that they line up nicely . The controls themselves are listed in Table 9-6.

Table 9-6: Controls on the login.aspx Page

Control

Properties

TextBox

ID=txtUsername

RequiredFieldValidator

ErrorMessage=Username required; ControlToValidate=txtUsernam e

TextBox

ID=txtPassword; TextMode = Password

RequiredFieldValidator

ErrorMessage=Password required; ControlToValidate=txtPassword

Button

ID=btnLogin; Text = Login

Given that our business objects provide business rules, including validation, why are we including the RequiredFieldValidator controls here to validate the input? Typically, duplication of logic is a bad thing, but when building web interfaces, we're faced with tough choices. Since our business objects can't run on the client in the browser, we can only use the objects to validate input once the user has submitted the form back to the server. That doesn't provide a very responsive or pleasant user experience.

The only realistic way to overcome this issue and provide a responsive user experience is somehow to "extend" some of the validation processing out to the browser, which means duplicating some business logic. We can't allow the UI to be the final check because our objects service other interfaces and we want to ensure that the rules are enforced constantly, so we duplicate some subset of the logic.

Note  

This isn't ideal, but it's a trade-off. We increase the cost of maintenance and the chance of introducing bugs over time in order to provide the user with a richer experience.

Whether we want to use this technique depends on our application's requirements, and our willingness to deal with the long- term maintenance issues. In any case, our business objects will validate any data that we attempt to put into the object. In the case of our login form, the business object we'll be using is the BusinessPrincipal from the CSLA .NET Framework. To make it easier to code, we'll import a couple of namespaces into the form's code, such as these:

  using CSLA.Security; using System.Threading; using System.Security.Principal; using System.Web.Security;  

Then we can write code to respond when the user clicks the Login button, such as the following:

  private void btnLogin_Click(object sender, System.EventArgs e)     {       string userName = txtUsername.Text;       string password = txtPassword.Text;       // if we're logging in, clear the current session       Session.Clear();       // log in to the system       BusinessPrincipal.Login(userName, password);       // see if we logged in successfully       if(Thread.CurrentPrincipal.Identity.IsAuthenticated)       {         Session["CSLA-Principal"] = Thread.CurrentPrincipal;         HttpContext.Current.User = Thread.CurrentPrincipal;         // redirect to the page the user requested         FormsAuthentication.RedirectFromLoginPage(           userName, false);       }     }  

The core of this code shows how we do the authentication using BusinessPrincipal , and it's the same as our code in the Windows Forms UI, as shown here:

  BusinessPrincipal.Login(userName, password);  

This method call causes our CSLA .NET code to check the username and password values against our security database. The result is that the current thread will have a BusinessPrincipal object that we can check to see if the user was successfully authenticated or not.

However, there's a lot more going on here. We need to deal with the requirements of the Web Forms environment. For example, we may get to this form because the user is just arriving, or we could get here because he was already using the system and navigated here by hand. To ensure that we start with a clean slate, we clear our session state before doing anything else. This ensures that we don't have an old Principal object for this user, or some other user, in Session . Then we attempt to log in using BusinessPrincipal . If that succeeds, we store the resulting BusinessPrincipal object as part of our session state, as follows:

 if(Thread.CurrentPrincipal.Identity.IsAuthenticated)         {           Session["CSLA-Principal"] = Thread.CurrentPrincipal; 

This is important because the CurrentPrincipal object contains the list of roles to which the user belongs, and we'll need that information on subsequent pages. There's nothing in the ASP.NET security infrastructure to keep this object around from page to page, so it's up to us to retain it. In this case, we're retaining it in the Session object.

Tip  

Though forms-based security does keep track of the fact that the user is authenticated, it has no built-in provision for keeping track of extra information (like the user's list of roles).

By putting the BusinessPrincipal object into Session , we can easily retrieve it on each page request by adding some code into Global.asax ”we'll add that code shortly. The key here is that we must have a way to restore the BusinessPrincipal object before each page request, and Session allows us to keep the object around from page to page.

If the user did successfully log in, we also make sure that the ASP.NET environment is using the correct principal object. The ASP.NET environment manages its security via the HttpContext object, which is available to all code running within ASP.NET. We need to set its User property to our BusinessPrincipal object, as shown here:

 HttpContext.Current.User = Thread.CurrentPrincipal; 

This makes the correct principal object available via HttpContext.Current.User , and also via the User property in each web form.

Finally, now that we know the user is authenticated and we've stored the BusinessPrincipal object for later use, we can have the forms-based security mechanism allow the user to continue on to the page she was trying to access, as follows:

 // redirect to the page the user requested           Security.FormsAuthentication.RedirectFromLoginPage(             userName, false); 

If the user accessed our login form directly, it redirects them to the default page for the application. It also generates a cookie that contains a user ticket or hashed key value that identifies the user to ASP.NET. ASP.NET automatically generates and manages this cookie on our behalf ; if it's lost or becomes invalid, ASP.NET automatically redirects the user back to Login.aspx to be authenticated again.

Tip  

ASP.NET also provides an option to put the ticket into the URL instead of a cookie. This can be important if we want to support users who turn off cookie support in their browsers. The unfortunate side effect of this approach is that the URL always contains a chunk of encrypted data, thereby making it difficult for end users to add the URL to his Favorites list, or sending the URL to another user. In either case, subsequent use of that URL will be invalid because the ticket will be invalid. The result is that the user of this URL will be redirected to the login page to authenticate.

Global.asax

Once the user is logged in, we store the BusinessPrincipal object in Session , thereby making it available to subsequent pages. However, our CSLA .NET Framework code is designed to use standard .NET security by accessing the thread's CurrentPrincipal property. It isn't for looking at ASP.NET-specific features like Session . Additionally, ASP.NET won't automatically maintain the HttpContext principal object from page to page; it's up to us to make sure it's set properly at the start of each page request.

This means that before the code in our individual Web Forms runs, we should ensure that the thread's CurrentPrincipal property and the HttpContext User property contain our BusinessPrincipal object. ASP.NET provides a standard mechanism for us to do this through the Global.asax file.

Tip  

If you're using Windows' integrated security, this step isn't needed. The thread and HttpContext principal objects will be automatically set by ASP.NET.

Global.asax contains methods that respond when events are raised by the ASP.NET environment. We can respond to events that indicate that the application has started, that a page request has begun or ended, or that the Session object has been loaded just before page processing begins.

In our case, we want to respond to the last of these. When this event fires, we have access to the Session data, and we know we're running before any code in the Web Form itself. Open the Global.asax file, and add a couple of using statements, as follows:

  using System.Threading; using System.Security.Principal;  

Then add a method to handle the AcquireRequestState event, as shown here:

  protected void Global_AcquireRequestState(       object sender, EventArgs e)     {       // set the security principal to our BusinessPrincipal       if(Session["CSLA-Principal"] != null)       {         Thread.CurrentPrincipal =           (IPrincipal)Session["CSLA-Principal"];         HttpContext.Current.User = Thread.CurrentPrincipal;       }       else       {         if(Thread.CurrentPrincipal.Identity.IsAuthenticated)         {           System.Web.Security.FormsAuthentication.SignOut();           Server.Transfer("Login.aspx");         }       }     }  

The first part of this is pretty straightforward: If the Session object contains our BusinessPrincipal , make it the current principal object for the thread and HttpContext , as shown here:

 if(Session["CSLA-Principal"] != null)       {         Thread.CurrentPrincipal =           (IPrincipal)Session["CSLA-Principal"];         HttpContext.Current.User = Thread.CurrentPrincipal;       } 

The second part isn't so obvious, but it solves a critical problem. If Session is configured to run in process with our Web Forms (which is the default), and our AppDomain gets reset, then the Session object is destroyed . However, the user's authentication ticket (which is automatically stored in a cookie by ASP.NET) is not destroyed, so the user can continue to access the pages of our application.

Of course, without a proper BusinessPrincipal object, we can't check security, and neither can our business objects or the DataPortal . In fact, without a proper BusinessPrincipal object, the application will crash at some point. This means that if we find no BusinessPrincipal in the Session object, but the user is already authenticated, then we know we're in trouble. The solution is to sign the user out and redirect them to the login form so that we can reauthenticate them and get a new BusinessPrincipal object, as shown here:

 System.Web.Security.FormsAuthentication.SignOut();           Server.Transfer("Login.aspx"); 

From the user's perspective, this is rather inconvenient, not to mention confusing. If it happens often, we may want to consider running the Session object in its own process or on a separate state server machine, as discussed earlier in the chapter.

At this point, we should be able to build the project and navigate to the site. We'll be redirected to the login form, where we can provide a valid username and password, after which we'll end up on the default page with our name displayed in the label near the top as shown in Figure 9-10.

image from book
Figure 9-10: Example display from default.aspx

Since we haven't implemented the Projects or Resources forms yet, the links won't work, but we've established that our security infrastructure works, so we can move on.

Projects Form

From Default.aspx , the user can choose to work with either projects or resources. If he chooses the former, we'll direct him to a form that displays a list of projects. From there, the user can add, remove, or edit projects ( assuming he has security clearance to do so).

Setting Up the Form

Add a new web form to the project and name it Projects . Set its properties as shown in Table 9-7.

Table 9-7: Properties for the projects.aspx Page

Property

Value

pageLayout

FlowLayout

enableViewState

false

With the controls added, the form should look similar to Figure 9-11.

image from book
Figure 9-11: Layout of the projects.aspx web form

The title is simple text formatted with the Header 1 style. The controls are detailed in Table 9-8.

Table 9-8: Controls on the projects.aspx Page

Control

Properties

HyperLink

Text=Home; NavigateUrl=Default.aspx

LinkButton

Name=btnnewProject; Text=Add new project

DataGrid

Name=dgProjects; EnableViewState=false

We set the EnableViewState property of the DataGrid control to false because we'll rebind it to the ProjectList object each time the page is loaded. When displaying potentially large amounts of data in a grid control, it's often better to reload the grid's data from the source than to send a large amount of page state to and from the browser on each page request. This approach also ensures that the user gets current data displayed on each page request so that changes to the data are reflected in the display with each page refresh.

The DataGrid control requires a bit more setup than this, since we'll be using data binding to populate it from our ProjectList business object. Right-click the DataGrid control and select the Property Builder option. This brings up a Properties dialog box, where you should select the Columns tab. This tab allows us to add and manipulate the columns included in the grid control.

First, uncheck the Create Columns Automatically at Run Time box, which allows the grid to generate columns dynamically, based on the data source. We want more control over the process. We'll add two columns to the control for the ID and Name values, and one for a Remove hyperlink. The ID column should look similar to Figure 9-12.

image from book
Figure 9-12: Properties for the Project ID column

We're creating a simple bound column that's bound to the ID property of the data source, but isn't visible. We'll need this ID value to implement our code, but the user doesn't need to see our internal GUID values.

The name column is a button column and should look similar to Figure 9-13.

image from book
Figure 9-13: Properties for the Project Name column

This column doesn't only display the Name property from the data source ”it's also a hyperlink-style button. The Select command is special, thereby causing the DataGrid to raise the SelectedIndexChanged event when an item in this column is clicked. Later on, we'll write code to react to this event and take appropriate action.

The remove column is also a button column as shown in Figure 9-14.

image from book
Figure 9-14: Properties for the Remove column

In this case we aren't binding the display to a property, but we are providing Remove as the constant text to display. The command name is Delete , which will trigger the generic DeleteCommand event to be raised by the grid control. We can write code in the handler for this event to take appropriate action.

Click OK to close the Properties dialog box, then right-click the DataGrid control, choose Auto Format, and format the control to use the Colorful 2 scheme as shown in Figure 9-15.

image from book
Figure 9-15: Choosing the Colorful 2 formatting scheme

Coding the Form

At this point the form is ready, and we can move on to writing the code that makes it work.

Loading Data

When the form is first loaded, we need to do a couple of things. First, we need to load the DataGrid control with a list of projects. Getting the list of projects is easy, because we can use our ProjectList business object. Populating the control is also easy, because we can just use data binding, as shown here:

 private void Page_Load(object sender, System.EventArgs e)       {  dgProjects.DataSource = ProjectList.GetProjectList();         DataBind();  } 

All we do is get a new ProjectList object and set it as the control's DataSource . We then call the page's DataBind() method to trigger the data-binding process. Our grid control has predefined columns, so it knows which columns to bind to which properties from the data source.

Tip  

Remember that our ProjectInfo structure has no public variables, but was implemented with property methods instead. This extra work was required specifically because Web Forms data binding won't bind to variables of a class or structure ”it will only bind to property methods.

As shown in Figure 9-16, when the page is accessed, the DataGrid will display the list of project data from the ProjectList object.

image from book
Figure 9-16: Example display from the project.aspx web form

From here we can view, edit, and remove Project objects.

Security

The other thing we need to do as the page is loaded is deal with security. Specifically, we want to ensure that only the appropriate options are displayed, based on the user's role.

Tip  

In web development this is often called personalization , but it's the same good UI design practice from Windows development, applied to the web environment.

Since we've already built all the code to make sure that our thread's security principal object is a valid BusinessPrincipal , we can use standard .NET security code to hide or show the UI elements, as shown here:

 private void Page_Load(object sender, System.EventArgs e)     {       dgProjects.DataSource = ProjectList.GetProjectList();       DataBind();       // set security  btnNewProject.Visible = User.IsInRole("ProjectManager");       dgProjects.Columns[2].Visible =         HttpContext.Current.User.IsInRole("ProjectManager")          HttpContext.Current.User.IsInRole("Administrator");  } 

We set the Visible properties on the link to add a new project, and on the column to remove a project, based on the user's role. This is directly analogous to the security code we put into our Windows Forms UI.

Technically, this code is optional, as the business objects themselves already include code to ensure that the user must be in the right role to perform these actions. However, good UI design dictates that a user should only have the option to do things that they can actually do , and so it's good practice to include some security code in the UI to hide or disable invalid options.

Even if we don't implement the security in the UI, or if the user bypasses this security by typing in a URL to navigate directly to a page, our business objects include logic to prevent the user from doing anything invalid.

Selecting a Project

The DataGrid control has a button column with the Select command. If the user clicks an element in this column, we'll get a SelectedIndexChanged event from the control. We can handle this event to navigate to another page where the user can view or edit that particular project, as follows:

 private void dgProjects_SelectedIndexChanged(object sender,       System.EventArgs e)     {  Guid id = new Guid(dgProjects.SelectedItem.Cells[0].Text);       Session["Project"] = Project.GetProject(id);       Response.Redirect("ProjectEdit.aspx");  } 

To do this, we need to know the ID value for the project the user selected. The first (invisible) column in the DataGrid control contains this value, and we can retrieve it with expressions like the following:

 Guid id = new Guid(dgProjects.SelectedItem.Cells[0].Text); 

The ID value is text representing the GUID for a Project object, so we convert it to a Guid variable and use it to retrieve the Project object. This Project object is then placed in Session so that it will be available to the next page, which is ProjectEdit.aspx (we'll build this shortly). To get there, we use Response.Redirect() to cause the user's browser to navigate to that form.

Tip  

We could also use Server.Transfer() to navigate to the ProjectEdit form, but then the browser would not display the correct URL for the ProjectEdit page, which could be confusing for the user. Additionally, Server.Transfer() doesn't allow us to pass parameters as part of the URL. Though we're not using that feature here, it's a limitation to be aware of. For these reasons, I'll be using Response.Redirect() throughout this application.

Now, when the user clicks on a project in the DataGrid , we'll bring up the ProjectEdit form with the selected project displayed for viewing or editing.

Removing a Project

Similarly, the user can click the Remove link in a row, and then we need to remove that project from the system in response. When she performs the action, we'll get a DeleteCommand event from the control. In our event handler code, we simply remove the selected project, as shown here:

  private void dgProjects_DeleteCommand(object source,       System.Web.UI.WebControls.DataGridCommandEventArgs e)     {  Guid id = new Guid(e.Item.Cells[0].Text);       Project.DeleteProject(id);  dgProjects.DataSource = ProjectList.GetProjectList();       DataBind();     }  

As before, we use the DataGridCommandEventArgs parameter passed to our code to find the ID value of the selected item, as follows:

 Guid id = new Guid(e.Item.Cells[0].Text);         Project.DeleteProject(id); 

We then rebind the DataGrid control to a new ProjectList object so that it reflects the fact that the project has been removed from the system, as shown here:

 dgProjects.DataSource = ProjectList.GetProjectList();         DataBind(); 
Adding a New Project

The final code we need to add will handle the click event of the LinkButton for adding a new Project object. When this button is clicked, we'll create a new Project object, and then navigate to the ProjectEdit form so the user can edit the new object. Add the following code to the form:

  private void btnNewProject_Click(object sender,       System.EventArgs e)     {       Session["Project"] = Project.NewProject();       Response.Redirect("ProjectEdit.aspx");     }  

We simply use the newProject() method of the Project class to create a new Project object. We put this object into Session so that it will be available to the ProjectEdit form ”just like we did earlier for editing a Project . Then we navigate to the ProjectEdit page, where the user can add his data to the object.

The Projects form is now complete, and you should be able to build the project and navigate to it to see a list of projects. The Remove button on each row will also work. Let's move on to implement the ProjectEdit form, so that we can successfully select a project to view or edit.

ProjectEdit Form

ProjectEdit is the main form for adding, viewing, and editing projects. We'll get here because the user clicked the link to add a new project in Projects.aspx , or because they clicked a link in that form to view or edit a specific project. Also, when we're done with the ResourceEdit form, the user will have a link from there to view or edit a specific project as well.

Setting Up the Form

Add a new web form and name it ProjectEdit . Change its properties as shown in Table 9-9.

Table 9-9: Properties of the ProjectEdit.aspx Page

Property

Value

pageLayout

FlowLayout

That was easy enough, but this form is actually rather more complex than the ones we've created so far.

Basic Layout

We'll use a standard HTML table to organize the columns, so that they line up nicely as shown in Figure 9-17.

image from book
Figure 9-17: Layout of the ProjectEdit.aspx web form
Control Details

The controls on the page are shown in Table 9-10.

Table 9-10: Controls on the ProjectEdit.aspx Page

Control

Properties

HyperLink

Text=Home; NavigateUrl=Default.aspx

HyperLink

Text=Project list; NavigateUrl=Projects.aspx

LinkButton

ID=btnnewProject; Text=Add new project

TextBox

ID=txtID; ReadOnly=true

TextBox

ID=txtName

RequiredFieldValidator

ErrorMessage=Name is required;
ControlToValidate=txtName

TextBox

ID=txtStarted

CompareValidator

ErrorMessage=Must be earlier than ended;
ControlToCompare=txtEnded;
ControlToValidate=txtStarted;
Operator=LessThanEqua l

TextBox

ID=txtEnded

CompareValidator

ErrorMessage=Must be later than started;
ControlToCompare=txtStarted;
ControlToValidate=txtEnded;
Operator=GreaterThanEqual

TextBox

ID=txtDescriptio n ; TextMode=MultiLin e

DataGrid

ID=dgResources; EnableViewState=false

LinkButto n

ID=btnAssignResource; Text=Assign resourc e

Button

ID=btnSave; Text=Save

Button

ID=btnCance l ; Text=Cancel

This sets the basic configuration of all the controls, but we need to take extra steps to set up the TextBox controls for data binding, and we need to configure the columns in the DataGrid control.

Data Binding the Controls

Data binding in Web Forms is quite different from data binding in Windows Forms. The biggest functional difference is that data binding in Web Forms is read-only ” it can be used to copy values from the data source into the control, but not from the control back into the data source. The dialog boxes that configure data binding are also quite different.

Each simple Web Forms server control, such as a TextBox , has a DataBindings property in the Properties window. If we click the " " button for the property, we'll get a dialog box in which we can specify what should be bound to the properties of the control.

To see how this works, select the txtID control and click the " " button for DataBindings . This dialog box is designed to simplify the process of binding the control to a data source. Unfortunately, it doesn't recognize our business objects as being valid data sources, so it isn't of great help to us. However, we can use it to specify a custom binding expression as shown in Figure 9-18.

image from book
Figure 9-18: DataBindings dialog box for txtID

This binding expression can refer to any data source available from our page's code, including business objects. For this expression to work, all we need to do is ensure that we expose a variable named _project that has an ID property. Since we know that ID is a GUID value, we're converting it to a string value by including the ToString() method.

In the HTML for the page, this translates to the following:

 <asp:TextBox id="txtID" runat="server"           Text="<%# _project.ID.ToString() %>"           ReadOnly="true" Width="245px" > 

The Text property of the control is set to the data-binding expression <%# _project.ID.ToString() %> . When our ASPX page is compiled, this will translate into code that retrieves the ID value from a form-level variable named _project .

Tip  

For this to work, we need to declare a form-level variable named _project , and ensure that it references an appropriate object before we invoke the data-binding mechanism on the page. We'll take care of these details when we get into the code behind the form.

Use this technique to set up custom binding expressions for the other text controls as shown in Table 9-11.

Table 9-11: Custom-Binding Expressions on the Controls

Control

Property

Expression

txtName

Text

_project.Name

txtStarted

Text

_project.Starte d

txtEnded

Text

_project.Ended

txtDescription

Text

_project.Description

By setting these properties on the controls, we avoid writing code to load data into the controls manually, or configure the data binding through code. This is nice, because it means that we can change the layout of the page ”including the binding of properties from the Project object to our controls ”without having to change the code behind the form.

Configuring the DataGrid Columns

As with the DataGrid control on the Projects form, this one requires more setting up in order to configure its columns and appearance. Right-click, choose Auto Format, and select the Colorful 2 scheme to get the appearance right. Then right-click and choose Property Builder to bring up the Properties dialog box so that we can configure the columns to be displayed.

In the Columns tab, deselect the option to create the columns automatically. Then add the columns shown in Table 9-12.

Table 9-12: Table Columns for the dgResources Control

Column Type

Properties

Bound

Header text=Resource ID ; Data Field=ResourceID ; Visible=false

Bound

Header text=First name ; Data Field=FirstName

Bound

Header text=Last name ; Data Field=LastName

Bound

Header text=Assigned ; Data Field=Assigned

Button

Text field=Role ; Command name=SelectRole

Button

Text=Remove ; Command name=Delete

Button

Text=Details ; Command name=Select

This configuration provides us with columns to display the name, assigned date, and role for each resource assigned to the project. The data will come from the ProjectResources collection associated with our Project object, so we can just use data binding to populate the grid control.

We've made the Role column into a button column, so the user can click on the role of a resource to indicate that he wants to change it. There are also a couple of columns that allow the removal of a resource, and that view or edit the details of a resource.

Coding the Form

At this point, we're ready to write the code behind the form. We'll need to load data when the form is loaded, and react to the events that are raised when the user clicks the various controls on the page.

Loading Data

As we noted earlier, this page may be reached from a number of locations within the UI as a whole. We may get here via the Add New Project link on the Projects page, or because the user clicked a link to edit an existing project. The user could also navigate here directly in her browser, without going through any previous pages. She might even get here by pressing the Back button in her browser.

In the Windows Forms interface, we relied on the form that called into ProjectEdit to set up a Project object before moving to ProjectEdit ”and we'll do the same thing here. In our Projects page, we included code to load a Project object with data and put it in Session before navigating to our ProjectEdit page. This means that by the time we get here, we've already got a Project object ready to edit.

We can also get here and not have a Project object. This can happen because the user navigated to this page directly, or pressed the Back button in the browser. In the former scenario, we'll assume that the user wants to add a new Project . If he used the Back button, we'll try to reload the object he was editing on this page last time he was here.

In any case, we need to remember that all our code will be interacting with a Project business object in order to do its work. To make this object available to all the code in our form, we'll declare a form-level variable and load it with the Project object each time the form loads. Add the following form-level variable declaration:

  // we make this a page-level variable and then       // always load it in Page_Load. That way the object       // is available to all our code at all times       protected Project _project;  

Between pages, we'll store the Project object in Session , but while the page is being processed we'll put it here for easier access by our code. Notice that this variable is declared with protected scope; there's a good reason for this. You'll remember that we used custom data-binding expressions to bind our controls to the _project variable when we designed the form. For this to work, the HTML portion of the page must have access to the _project variable, which means that it must be protected , internal , or public . Good programming practice dictates that we should only expand the scope as much as needed, and the most restrictive scope that still works is protected .

We can now create the Load event handler to initialize this variable each time the page loads, as follows:

 private void Page_Load(object sender, System.EventArgs e)     {  // get the existing Project (if any)       _project = (Project)Session["Project"];       if(_project == null)       {         // got here via either direct nav or Back button         if(txtID.Text.Length == 0)         {           // got here via direct nav           // we are creating a new project            _project = Project.NewProject();         }         else         {            // we've returned here via the browser's Back button            // load object based on ID value            _project = Project.GetProject(new Guid(txtID.Text));         }         Session["Project"] = _project;       }       // bind the grid every time (we're not using       // viewstate to keep it populated)       dgResources.DataSource = _project.Resources;       if(IsPostBack)         dgResources.DataBind();       else         DataBind();       // set security       if(!User.IsInRole("ProjectManager"))       {         btnNewProject.Visible = false;         btnSave.Visible = false;         btnAssignResource.Visible = false;         dgResources.Columns[5].Visible = false;       }  } 

The first thing we do is attempt to retrieve the object as shown here:

 // get the existing Project (if any)       _project = (Project)Session["Project"]; 

There are four scenarios we may encounter here:

  • The user may have navigated here directly.

  • The user may have pressed the Back button in the browser.

  • This may be a postback of the current page.

  • The user may have navigated here from another page.

In the first two scenarios, Session won't contain a Project object. In that case, we can check the value of the txtID field to determine if the user navigated here directly, or used the Back button. If the latter, the field will contain the ID value of the Project object she was editing at that time, so we can attempt to reload it. Otherwise, we'll simply create a new Project object for the user to edit:

 // got here via either direct nav or Back button         if(txtID.Text.Length == 0)         {           // got here via direct nav            // we are creating a new project            _project = Project.NewProject();         }         else         {            // we've returned here via the browser's Back button            // load object based on ID value            _project = Project.GetProject(new Guid(txtID.Text));         }         Session["Project"] = _project; 

At this point, we know that we have a Project object to work with ”either a preexisting one we're editing, or a new one we're creating. We put that object into Session to make sure that it's available on subsequent page requests , and then we bind the DataGrid to the object's Resources collection, as follows:

 // bind the grid every time (we're not using         // viewstate to keep it populated)         dgResources.DataSource = _project.Resources; 

Notice that this is done regardless of whether this is a postback. The reason for this is that we've set EnableViewState to false for this control, so that its state isn't transferred to and from the browser on each page request. Though that cuts down on network traffic, it does mean that we need to rebind the control each time.

Tip  

In doing this, we're trading bandwidth against server processing. We've decreased the size of our HTML output, but we have to reload the control with data on each page request. In almost every case, processing speed is faster than bandwidth, so this is a good trade-off. If your server is so slow that bandwidth isn't the bottleneck, you can change this code to avoid rebinding the data on postback (though if your server is actually that slow, you're probably better off getting a bigger server, or implementing a web farm).

Finally, we trigger the data-binding process to populate the display on the form, as follows:

 if(IsPostBack)           dgResources.DataBind();         else           DataBind(); 

In the case of a postback, our regular controls will already have their values, so only the grid needs to be rebound. Otherwise, this is the first time the user has hit the page, and so we need to populate all the controls. For this, we call the page's global DataBind() method.

Security

There's one more thing that we need to do as the form is loaded, and that's to set security based on the user's role. While many types of users can use this page to view data, only people in the ProjectManager role can actually edit a Project object. We'll check the user's role and (conditionally) hide controls that would allow the user to save any changes to the object.

Add the following code to the bottom of the Page_Load() method:

  // set security       if(!User.IsInRole("ProjectManager"))       {         btnNewProject.Visible = false;         btnSave.Visible = false;         btnAssignResource.Visible = false;         dgResources.Columns[5].Visible = false;       }     }  

As we did in the Projects form, we check the Principal object to see if the user is in a valid role. If the user isn't in the ProjectManager role, we hide all the controls that allow the user to change the Project object permanently.

With this done, the form should now load and display either the default values for a new project, or the existing values from a project that was loaded from the database.

Saving the Form

If the user clicks the Save button, he expects that any changes he's made on the form will be stored in the database. On the surface, this seems like a simple thing, since all we need to do is call the Save() method on our Project object. However, we need to keep in mind that Web Forms data binding is read-only ; it's up to us to copy the values from the form back into the object. To do this, we can create a helper function as follows:

  private void SaveFormToObject()     {        _project.Name = txtName.Text;       _project.Started = txtStarted.Text;       _project.Ended = txtEnded.Text;       _project.Description = txtDescription.Text;     }  

This code isn't complex: For each value, we simply copy the value from the form into the object. Given this helper method, we can easily implement the code for the Save button's Click event handler, as shown here:

  private void btnSave_Click(object sender, System.EventArgs e)     {       SaveFormToObject();       _project = (Project)_project.Save();       Session.Remove("Project");       Response.Redirect("Projects.aspx");     }  

Here the form's contents are saved into the object and the object is then saved into the database by calling its Save() method. With that done, we navigate the user back to the Projects form. At this point, we no longer need the Project object ”it has been saved to the database, so we remove it from Session . This is good practice because it helps minimize the amount of data in Session , thereby improving performance if Session is being kept out of process or on a state server machine.

Canceling the Form

If the user clicks the Cancel button, things are simpler. All we need to do in that case is abandon the Project object and navigate the user back to the Projects form, as follows:

  private void btnCancel_Click(object sender, System.EventArgs e)     {       Session.Remove("Project");       Response.Redirect("Projects.aspx");     }  

Here too we're minimizing the amount of data in Session by removing the object now that we're done with it.

Adding a New Project

Just like we did in the Projects form, we have a LinkButton that the user can click if she wants to add a new Project object. Unsurprisingly, the following code will be the same as in the Projects form:

  private void btnNewProject_Click(       object sender, System.EventArgs e)     {       Session["Project"] = Project.NewProject();       Response.Redirect("ProjectEdit.aspx");     }  

We simply create a new Project object by calling the NewProject() method, and then navigate to the ProjectEdit form so that the user can edit this new object.

Adding a Resource

Our form also includes a LinkButton control that allows the user to assign a resource to the project that's currently being edited. If the user clicks this button, we'll bring up a form where he can select the resource and indicate the role that the resource will play in the project. This new form will be named AssignResource , and it will act rather like a dialog box in the sense that we'll come here from another page and return to that page when this one is complete.

Before we can navigate from ProjectEdit to the AssignResource dialog form, we need to save the contents of our form into the Project object. If we don't do this, any changes the user has made on our form will be lost, since only the Project object is persistent across pages, through the Session object, as shown here:

  private void btnAssignResource_Click(       object sender, System.EventArgs e)     {       SaveFormToObject();       Response.Redirect("AssignResource.aspx");     }  

We save the form's contents to the object by calling the SaveFormToObject() method that we created earlier. That copies the values from the form into _project , and then updates Session so that it has a reference to the updated object. With that done, we can safely navigate to the AssignResource form.

Selecting a Resource

Within the DataGrid control that displays the resources assigned to the project, we included a Details column. If the user clicks a Details button in the grid, a SelectedIndexChanged event will be raised. We can respond to this event to display details about the resource by navigating to the ResourceEdit form (which we'll create shortly).

When this happens, the user is navigating away from editing a project, and will then be editing a resource. From our point of view, this is a huge shift, and there are various ways we can handle it. For this implementation, we'll say that this action is the same as the user first clicking the Save button, and then navigating to ResourceEdit ”this way, the user's changes will be retained.

If the user doesn't have security clearance to change a Project object, we'll simply drop the Project object and navigate the user to the ResourceEdit form, as shown here:

  private void dgResources_SelectedIndexChanged(       object sender, System.EventArgs e)     {       // check security       if(User.IsInRole("ProjectManager"))       {         // only do save if user is in a valid role         SaveFormToObject();         _project = (Project)_project.Save();       }       Session.Remove("Project");       string id = dgResources.SelectedItem.Cells[0].Text;       Session["Resource"] = Resource.GetResource(id);       Response.Redirect("ResourceEdit.aspx");     }  

First, we check to see if the user is a ProjectManager . If so, we save any changes to the Project object, as shown here:

 // check security       if(User.IsInRole("ProjectManager"))       {         // only do a save if user is in a valid role         SaveFormToObject();         _project = (Project)_project.Save();       } 

When the user clicks on a resource, we're treating it as though she clicked Save or Cancel to exit the ProjectEdit form, and then navigated to a specific resource from the Resources form. Anytime the user clicks Save or Cancel on ProjectEdit , we remove the Project object from Session to save memory, and so we'll do that here as well:

 Session.Remove("Project"); 

Then, whether there's a ProjectManager or not, we navigate the user to the ResourceEdit page so that he can view or edit the selected Resource object, as follows:

 string id = dgResources.SelectedItem.Cells[0].Text;       Session["Resource"] = Resource.GetResource(id);       Response.Redirect("ResourceEdit.aspx"); 
Changing a Role

The user might also click the Role column in the grid control to indicate that she wants to change the role the resource plays on the project. When this column is clicked, we'll get an ItemCommand event from the grid control, where we can process the request.

ItemCommand is a generic event that can be raised for more than one column in the grid control. When we set up the Role column, we specified that the command text was SelectRole . We can check for that in our code, as follows:

  private void dgResources_ItemCommand(object source,       System.Web.UI.WebControls.DataGridCommandEventArgs e)     {       if(e.CommandName == "SelectRole")       {         string id = e.Item.Cells[0].Text;         Session["ProjectResource"] = _project.Resources[id];         Session["Source"] = "ProjectEdit.aspx";         Response.Redirect("ChooseRole.aspx");       }     }  

If the user clicks an item in the Role column, we retrieve the ID value of the item from the grid and use it to locate the specific ProjectResource object from _project . This value is placed into Session so that the ChooseRole form can tell which object to edit, as follows:

 Session["ProjectResource"] = _project.Resources[id]; 

We also record the name of our current form in Session , as follows:

 Session["Source"] = "ProjectEdit.aspx"; 

This is done because ChooseRole will be used not only from ProjectEdit , but also from ResourceEdit . When the user has selected the new role for the resource, the ChooseRole form needs to know which form the user came from so it can return him to that form.

Finally, we navigate to the ChooseRole form, which we'll create later in the chapter.

Removing a Resource

The final action that the user might select is to click the Remove link in the DataGrid to indicate that she wants to remove a resource from the project. When she does this, we'll get a DeleteCommand event. We can remove the item in the event handler, as follows:

  private void dgResources_DeleteCommand(object source,       System.Web.UI.WebControls.DataGridCommandEventArgs e)     {       string id = e.Item.Cells[0].Text;       _project.Resources.Remove(id);       // rebind grid to update display       dgResources.DataSource = _project.Resources;       dgResources.DataBind();     }  

We retrieve the resource's ID value from the first column of the DataGrid , and use it to call the Remove() method of our Resources collection. This will remove the item from the collection, based on the business logic we wrote in our object. Once this is done, we need to rebind the DataGrid control to the Resources collection so that it reflects the fact that the entry has been removed.

At this point, the ProjectEdit form is functional, and should appear similar to Figure 9-19.

image from book
Figure 9-19: Example display from the ProjectEdit.aspx page

Of course, we still have the not insignificant limitation that the user can't navigate to the ChooseRole , AssignResource , or ResourceEdit forms, because we haven't built them yet!

The ChooseRole Form

The ChooseRole form is a "dialog" form in that it's intended to be used from the ProjectEdit or ResourceEdit forms so that the user can change the role of a resource on a project. When the user is done with this form, it will navigate them back to the form where he started so that he can continue to edit his project or resource.

Tip  

We've only created ProjectEdit so far, but ResourceEdit will also navigate the user to ChooseRole if he wants to change the role of a resource on a project. We'll write code to support both scenarios as we create this form.

Setting Up the Form

Add a new web form to the project and name it ChooseRole . Set its pageLayout property to FlowLayout , and add a table and controls as shown in Figure 9-20.

image from book
Figure 9-20: Layout of the ChooseRole.aspx web form

An HTML table is again used to get the controls to line up nicely, and the meaningful controls are listed in Table 9-13.

Table 9-13: Controls on the ChooseRole.aspx Page

Control

Properties

Label

ID=lblLabel

Label

ID=lblValue

ListBox

ID=lstRole s

Button

ID=btnUpdate ; Text=Update role

Button

ID=btnCancel ; Text=Cancel

Since this form has no DataGrid , that's all there is to the setup of the controls!

Coding the Form

The code for this form isn't terribly complex, though it might appear that way at first! Remember that the user may reach this form from either ProjectEdit or ResourceEdit , and so we may be working with a ProjectResource or a ResourceAssignment child object respectively. This leads to a bit of extra coding, since we need to handle each scenario appropriately.

Loading the Roles

When the user first arrives at this page, we need to load the form with data. The ListBox control will contain the list of valid roles, and the two Label controls at the top will display some context information so that the user knows what she's doing on the form. Let's start by loading the ListBox control with the valid roles, as follows:

 private void Page_Load(object sender, System.EventArgs e)     {  if(!Page.IsPostBack)       {         foreach(string role in Assignment.Roles)  lstRoles.Items.Add(Assignment.Roles[role]);       }     } 

Although it would be nice to use data binding here, we can't bind to a NameValueCollection , which is what we get from the Roles.GetCollection() method. Instead, we simply loop through the collection, putting each value into the ListBox control.

Tip  

This is a case in which we might opt to include a GetArray() method in the Roles class in order to provide the data in a bindable format.

Notice that we only do this the first time the user hits the page ”after that, we rely on the page's view state to maintain the list of values. This is important, since we're using the value selected by the user to change the object's value when the user clicks the Update button. If we reloaded the ListBox control with data on a postback, we'd wipe out the user's selection, thereby preventing the form from functioning properly.

Loading Data from the Child Object

Now we can move on to deal with the actual business object. This isn't complicated, but it does require a security check. Remember that only a ProjectManager can edit a Project object, and only a Supervisor or a ProjectManager can edit a Resource , so we need to include the following checks in here as well:

 if(!Page.IsPostBack)       {         foreach(string role in Assignment.Roles)           lstRoles.Items.Add(Assignment.Roles[role]);  if((string)Session["Source"] == "ProjectEdit.aspx")         {           // we are dealing with a ProjectResource           // check security           if(!User.IsInRole("ProjectManager"))           {             // they should not be here             SendUserBack();           }           ProjectResource obj =             (ProjectResource)Session["ProjectResource"];           lblLabel.Text = "Resource";           lblValue.Text = obj.FirstName + " " + obj.LastName;           SelectItem(lstRoles, obj.Role);         }         else         {           if((string)Session["Source"] == "ResourceEdit.aspx")           {             // we are dealing with a ResourceAssignment             // check security             if(!User.IsInRole("Supervisor") &&               !User.IsInRole("ProjectManager"))             {               // they should not be here               SendUserBack();             }             ResourceAssignment obj =               (ResourceAssignment)Session["ResourceAssignment"];             lblLabel.Text = "Project";             lblValue.Text = obj.ProjectName;             SelectItem(lstRoles, obj.Role);           }           else           {              // we came from an invalid location              Response.Redirect("Default.aspx");           }        }  } 

In either case, if we detect that the user isn't authorized to change the value, we redirect him back to where he came from by calling a SendUserBack() method, as follows:

  private void SendUserBack()     {       string src = (string)Session["Source"];       Session.Remove("Source");       Response.Redirect(src);     }  

This method retrieves the name of the page from which he came and navigates back to that page. You may remember that in ProjectEdit , before we navigated to ChooseRole , we recorded ProjectEdit.aspx in Session for this very purpose.

Assuming that the user is in the right role to be here, we get the appropriate business object and use it to populate the display. If she came from ProjectEdit , then we're editing a ProjectResource object, as shown here:

 ProjectResource obj =             (ProjectResource)Session["ProjectResource"];           lblLabel.Text = "Resource";           lblValue.Text = obj.FirstName + " " + obj.LastName;           SelectItem(lstRoles, obj.Role); 

We retrieve the ProjectResource object from Session , and then set the two Label controls to provide some context for the user. We also change the selected item in the ListBox control so that it matches the current Role value from the business object. This is done by calling a helper method, as follows:

  private void SelectItem(       System.Web.UI.WebControls.ListBox lst, string item)     {       int index = 0;       foreach(ListItem entry in lst.Items)       {         if(entry.Value == item)         {           lst.SelectedIndex = index;           return;         }         index++;       }     }  
Tip  

Note that in .NET 1.1 you can use the SelectedValue property of the ListBox control to more easily set the selected item. The code shown here is only required for .NET 1.0.

The equivalent procedure is followed if the user came from ResourceEdit .

Updating the Object

Once the user selects the new role from the ListBox control, she may click the Update button to indicate that she wants to save the change. In that case, we retrieve the correct child object, just like we did in the Load event handler. Then we update its Role property, and return to the original form ”either ProjectEdit or ResourceEdit , as appropriate:

  private void btnUpdate_Click(object sender, System.EventArgs e)     {       if((string)Session["Source"] == "ProjectEdit.aspx")       {         // we are dealing with a ProjectResource         ProjectResource obj =           (ProjectResource)Session["ProjectResource"];         obj.Role = lstRoles.SelectedItem.Value;       }       else       {         // we are dealing with a ResourceAssignment         ResourceAssignment obj =           (ResourceAssignment)Session["ResourceAssignment"];         obj.Role = lstRoles.SelectedItem.Value;       }       SendUserBack();     }  

For instance, if we're dealing with a ProjectResource object from the ProjectEdit form, we execute the following code:

 // we are dealing with a ProjectResource         ProjectResource obj =           (ProjectResource)Session["ProjectResource"];         obj.Role = lstRoles.SelectedItem.Value; 

We retrieve the ProjectResource object from Session , and then set its Role property to the value the user selected in the ListBox control. The user is then redirected back to the edit form from which he came by calling the SendUserBack() helper function that we created earlier. This procedure is possible because our business object was designed to display and accept human-readable values for the Role property. Not only doesn't the user have to see the cryptic ID values for the roles, but the UI developer doesn't need to deal with them either.

The same basic steps occur if the user is working with a ResourceAssignment child object.

Canceling the Update

If the user clicks the Cancel button, all we need to do is redirect her back to where she came from without first updating the business object's value, as shown here:

  private void btnCancel_Click(object sender, System.EventArgs e)     {       SendUserBack();     }  

With all of this code in place, the form will now display the list of roles so that user can change the role as required. This is illustrated in Figure 9-21.

image from book
Figure 9-21: Example display from the ChooseRole.aspx page

Remember that this form is just editing the business object in memory on the web server. Nothing is stored to the database until the user clicks the Save button on the main edit form or renavigates to this dialog box.

The AssignResource Form

The other "dialog" form that we need to create for project editing allows the user to assign a new resource to the project. As with ChooseRole , the user ends up at this form because he's editing a Project object; when he's done on this form, he'll be returned to the ProjectEdit form. Unlike ChooseRole , however, this particular "dialog" form only deals with ProjectResource child objects, so it's somewhat simpler.

Setting Up the Form

Add a web form to the project and name it AssignResource . Change its pageLayout property to FlowLayout and add controls as shown in Figure 9-22.

image from book
Figure 9-22: Layout of the AssignResource.aspx web form

This time, there's no HTML table on this page, because it's simple enough not to need one for organization. The controls are listed in Table 9-14.

Table 9-14: Controls on the AssignResource.aspx Page

Control

Properties

ListBox

ID=lstRoles

DataGrid

ID=dgResources ; EnableViewState=false

Button

ID=btnCancel ; Text=Cancel

Like our other DataGrid controls, this one requires a bit more configuration. Use the Property Builder dialog box to turn off autogeneration of columns, and then add the columns listed in Table 9-15 manually.

Table 9-15: Table Columns for the dgResources Control

Column Type

Properties

Bound

Header text=Resource ID ; Data Field=ID ; Visible=false

Button

Header text=Resource name ; Text field=Name ; Command name=Select

We'll use the DataGrid to display a list of names from the ResourceList business object. If the user clicks one of the names, we'll use the SelectedIndexChanged event to indicate that the user wants to assign that resource to the project.

Coding the Form

The code behind this form is some of the simplest we've seen!

Loading Data

When the form is loaded, we need to load the ListBox with the list of valid roles, and the DataGrid with the list of resources, as follows:

 private void Page_Load(object sender, System.EventArgs e)     {  // check security       if(!User.IsInRole("ProjectManager"))       {         // this user should not be here         Response.Redirect("ProjectEdit.aspx");       }       if(!Page.IsPostBack)       {         foreach(string role in Assignment.Roles)           lstRoles.Items.Add(Assignment.Roles[role]);         if(lstRoles.Items.Count > 0) lstRoles.SelectedIndex = 0;       }       dgResources.DataSource = ResourceList.GetResourceList();       dgResources.DataBind();     }  

Because we've left EnableViewState set to true for the ListBox this time, we only need to load the list of roles when the page is first accessed. As with the ChooseRole form, this is important because reloading the list of roles on a postback would prevent us from getting access to the user's selection.

The DataGrid , on the other hand, has EnableViewState set to false , so we need to reload its data on each page access. Assuming that we have a large amount of resources in our database, this is good practice because it reduces the amount of view state data transferred to and from the browser on each page request.

Assigning a Resource

When the user clicks a role in the ListBox , there's no postback to the page. However, when the user clicks a resource name in the DataGrid , the page does post back, and we receive a SelectedIndexChanged event. In the handler, we can determine the resource selected and the role the user that was clicked in the ListBox , as follows:

  private void dgResources_SelectedIndexChanged(       object sender, System.EventArgs e)     {       Project project = (Project)Session["Project"];       string id = dgResources.SelectedItem.Cells[0].Text;       project.Resources.Assign(id, lstRoles.SelectedItem.Value);       Response.Redirect("ProjectEdit.aspx");     }  

First, we retrieve the Project object from Session so that we can add the new resource to it. We then use the resource's ID value from the grid and the Role value from the ListBox as parameters to the Assign() method, thereby assigning a new resource to the Project object.

Once the resource has been assigned, we redirect the user back to the ProjectEdit form, where he can continue to edit the Project object.

Canceling the Action

If the user clicks the Cancel button, we simply redirect him back to the edit form, as shown here:

  private void btnCancel_Click(object sender, System.EventArgs e)     {       Response.Redirect("ProjectEdit.aspx");     }  

This completes all the functionality required in ProjectEdit (except for the provision of a ResourceEdit form) to allow the editing of a specific Resource object. We'll be coming to that shortly. Figure 9-23 shows an example of the AssignResource.aspx page.

image from book
Figure 9-23: Example output from the AssignResource.aspx page

Other than that, the user can add, edit, and view Project objects ”including assigning resources and changing their roles.

Resources Form

We're now most of way through the Web Forms UI, and the three remaining forms ” Resources , ResourceEdit , and AssignToProject ”are very similar to forms we've already created. Because of this, we'll skim through the creation of these forms quite rapidly .

The Resources form displays a list of the resources currently stored in the system, thereby providing a launching point from which the user can add, edit, or remove resources. Set up the Resources form just like the Projects form, as shown in Figure 9-24.

image from book
Figure 9-24: Layout of the Resource.aspx web form

The controls are listed in Table 9-16.

Table 9-16: Controls on the Resource.aspx Page

Control

Properties

HyperLink

Text=Home; NavigateUrl=Default.aspx

LinkButton

ID=btnnewResource; Text=Add new resource

DataGrid

ID=dgResources; EnableViewState=false

The DataGrid should be given the Colorful 2 scheme, and automatic column generation should be turned off. Then configure it with the following columns listed in Table 9-17.

Table 9-17: Table Columns for the dgResources Control

Column Type

Properties

Bound

Header text=Resource ID ; Data Field=ID ; Visible=false

Button

Header text=Resource name ; Text field=Name ; Command name=Select

Button

Text=Remove ; Command name=Remove

As the form loads, we need to populate the DataGrid with data from a ResourceList object. We also need to check the user's role to see if she should be able to add or remove Resource objects, as follows:

 private void Page_Load(object sender, System.EventArgs e)     {  dgResources.DataSource = ResourceList.GetResourceList();       DataBind();  // set security  btnNewResource.Visible =         User.IsInRole("Supervisor")          User.IsInRole("ProjectManager");       dgResources.Columns[2].Visible =         User.IsInRole("Supervisor")          User.IsInRole("ProjectManager")          User.IsInRole("Administrator");  } 

If the user selects a resource from the list, we can redirect him to the ResourceEdit form by loading a Resource object, putting it in Session , and navigating to ResourceEdit .

  private void dgResources_SelectedIndexChanged(       object sender, System.EventArgs e)     {       string id = dgResources.SelectedItem.Cells[0].Text;       Session["Resource"] = Resource.GetResource(id);       Response.Redirect("ResourceEdit.aspx");     }  

If the user clicks the Add New Resource link at the top of the page, we navigate to the ResourceEdit form. Note that this is different from what we do with ProjectEdit , as shown here:

  private void btnNewResource_Click(object sender, System.EventArgs e)     {       // make sure there's no active resource so ResourceEdit knows       // to add a new object       Session.Remove("Resource");       Response.Redirect("ResourceEdit.aspx");     }  

The difference is that we're not creating a new Resource object before navigating to ResourceEdit . This is because we can't create a new Resource object without first knowing the ID of the new object. The ResourceEdit form will be somewhat different from ProjectEdit in this regard since it needs to allow the user to enter the ID value for a new Resource manually.

ResourceEdit will assume that we're creating a new Resource if Session doesn't contain one so here we simply make sure that Session doesn't have a Resource object. Finally, if the user clicks the Remove button on a row, we'll need to remove that resource from the system, as follows:

  private void dgResources_DeleteCommand(object source,       System.Web.UI.WebControls.DataGridCommandEventArgs e)     {       string id = e.Item.Cells[0].Text;       Resource.DeleteResource(id);       dgResources.DataSource = ResourceList.GetResourceList();       DataBind();     }  

Here the DataGridCommandEventArgs parameter provides us with a reference to the row in the DataGrid on which the user clicked. We can use the first column in that row to retrieve the ID value, which we can then use to call the DeleteResource() method.

The result is a form that lists all the resources in the system as shown in Figure 9-25.

image from book
Figure 9-25: Example output from the Resources.aspx page

From here the user can edit or remove resources as needed.

The ResourceEdit Form

Like the ProjectEdit form, ResourceEdit is large and relatively complex. It displays all the data about a Resource object, including the list of projects to which the resource is assigned. From this form, the user can edit a Resource object as well as add, remove, or change the list of project assignments.

As we hinted earlier, there are some key differences in this form when compared to ProjectEdit , because of the way we create Resource objects. To create a new Resource object we must first have the user-supplied ID value of that object. This complicates matters because it means that we can't create a new Resource object until after the user has interacted with our form and entered an ID value.

To accommodate this, the form's Load event handler will detect whether we're adding a new Resource . If we are adding a new Resource , we'll make the ID control read-write so that the user can enter a value. Then, before attempting to save the data, we'll create a new Resource object using that ID value. Other than this change (and a related change in the Save button handler), however, the code in ResourceEdit will be very similar to that in ProjectEdit .

Setting Up the Form

Add the ResourceEdit form and set it up just like the ProjectEdit form earlier. It should look something like Figure 9-26.

image from book
Figure 9-26: Layout of the ResourceEdit.aspx web form
Control Properties

The controls are described in Table 9-18.

Table 9-18: Controls on the ResourceEdit.aspx Page

Control

Properties

HyperLink

Text=Home e ; NavigateUrl=Default.aspx

HyperLink

Text=Resource list;
NavigateUrl=Resources.asp x

LinkButton

ID=btnnewResource; Text=Add new resource

TextBox

ID=txtID; ReadOnly=true

RequiredFieldValidator

ErrorMessage=ID Required;
ControlToValidate=txtID

TextBox

ID=txtFirstName

RequiredFieldValidator

ErrorMessage=First name required;
ControlToValidate=txtFirstName

TextBox

ID=txtLastNam e

RequiredFieldValidato r

ErrorMessage=Last name required;
ControlToValidate=txtLastName

DataGrid

ID=dgProjects

LinkButton

ID=btnAssign; Text=Assign to project

Button

ID=btnSave; Text=Save

Button

ID=btnCance l ; Text=Cancel

Data Binding the Controls

As with ProjectEdit , in the code-behind page we'll declare a variable to hold our business object, as shown here:

  // We make this a page-level variable   // and then always load it in Page_Load   // That way, the object is available to all our code at   // all times   protected _resource As Resource  

We can then change the DataBindings property on each of our TextBox controls in order to bind them to the object. The bindings are shown in Table 9-19.

Table 9-19: Data-Binding Properties for the Controls

Control

Property

Expression

txtID

Text

_resource.ID

txtFirstName

Text

_resource.FirstName

txtLastName

Text

_resource.LastName

For example, the txtID DataBindings dialog box would appear as shown in Figure 9-27.

image from book
Figure 9-27: DataBindings dialog box for txtID

This isolates the display of data from our code, thereby making maintenance of the UI better overall.

Configuring the DataGrid Control

We also need to finish configuring the DataGrid control. Set its appearance to Colorful 2, and then set up the following columns shown in Table 9-20.

Table 9-20: Table Columns for the dgProjects Control

Column Type

Properties

Bound

Header text=Project ID ; Data Field=ProjectID ;
Visible=fals e

Bound

Header text=Name ; Data Field=Name

Bound

Header text=Assigned ; Data Field=Assigne d

Button

Text field=Role ; Command name=SelectRole

Button

Text=Remov e; Command name=Remove

Button

Text=Details ; Command name=Select

As in the ProjectEdit form, we've included a Role column that allows the user to navigate to the ChooseRole form in order to change the role of this resource on a project. We've already coded that form to handle requests from this form appropriately.

Coding the Form

What makes this form different from ProjectEdit is that the ID value of a Resource object is supplied by the user, rather than autogenerated. This complicates matters a bit, because it means that the txtID control is only read-only for an existing Resource object, but it needs to be read-write when we add a new Resource .

Tip  

This is only one possible design. We could do what we did in the Windows Forms UI and have the user first go to a form where she enters the ID value for the new resource, and then navigates to this page. I implemented it using a different approach here to demonstrate another possible variation.

The primary impact here is on the Page_Load() code, with a few side effects where we actually save the object.

Loading Data

When we load the form, we need to take into account that we may be creating a new Resource ”in which case, we won't have a business object right away, as shown here:

 private void Page_Load(object sender, System.EventArgs e)     {  // get the existing Resource (if any)       _resource = (Resource)Session["Resource"];       if(_resource == null)       {         // either we're adding a new object or the         // user hit the Back button         if(txtID.Text.Length == 0)         {           // we are adding a new resource           txtID.ReadOnly = false;           btnAssign.Visible = false;         }         else         {           if(txtID.ReadOnly)           {             // we've returned here via the browser's Back button             // load object based on ID value             _resource = Resource.GetResource(txtID.Text);             Session["Resource"] = _resource;           }           else           {             // we are adding a new resource             _resource = Resource.NewResource(txtID.Text);             Session["Resource"] = _resource;             txtID.ReadOnly = true;             btnAssign.Visible = true;           }         }       }       if(_resource != null)       {         // we have a resource to which we can bind         dgProjects.DataSource = _resource.Assignments;         if(IsPostBack)           dgProjects.DataBind();         else           DataBind();       }     }  

If we're to edit a Resource object, the object will have been loaded into Session by the previous form. This means that if we arrive at this form without a Resource object in Session , we know that one of the following conditions is true:

  • The user navigated here directly, and thus wants to add a resource.

  • The user clicked an Add New Resource link.

  • The user clicked the Back button.

As we did in ProjectEdit , we check for these conditions and set up the form appropriately. In this case, we can't simply create a Resource object to start with, since we don't yet know its ID value. Instead, if the user is adding a new Resource , we change the txtID control to be read-write so that the user can enter the ID value.

We can tell whether we're adding a new Resource by checking to see if txtID contains a value. If it's empty, then we know that this form doesn't represent an existing object, as shown here:

 if(_resource == null)       {         // either we're adding a new object or         // the user hit the Back button         if(txtID.Text.Length == 0)         {           // we are adding a new resource           txtID.ReadOnly = false;           btnAssign.Visible = false;         } 

In this case, we also hide btnAssign . Until we've created a Resource object, there's no way to assign it to any projects, so the button would be invalid at this stage.

If txtID does contain a value, then we know that one of the following two things is true:

  • The user hit the Back button and returned to this page.

  • The user is adding a new Resource , has entered an ID value, and then clicked the Save button, thereby causing a postback of this page to itself.

We can tell the difference between these two cases by checking whether txtID is read-only. If it's read-only and there's a value in txtID , then we know that the user pressed the Back button to get here. In this case, we use that ID value to reload the appropriate Resource object, as shown here:

 if(txtID.ReadOnly)           {             // we've returned here via the browser's Back button             // load object based on ID value             _resource = Resource.GetResource(txtID.Text);             Session["Resource"] = _resource;           } 

If there's a value in txtID and txtID isn't read-only, then we know that we were in the middle of adding a new Resource object and the user clicked the Save button. That button click causes a postback to our page so that the click event can be processed, but first we need to create a new Resource object by using the ID value in txtID , as shown here:

 else           {             // we are adding a new resource             _resource = Resource.NewResource(txtID.Text);             Session["Resource"] = _resource;             txtID.ReadOnly = true;             btnAssign.Visible = true;           } 

Now that we've determined whether we're editing, adding, or doing the postback after the user clicked Save, we can proceed. If we do have a Resource object, we need to bind the form's controls to the object, just like we did in ProjectEdit , as shown here:

 if(_resource != null)       {         // we have a resource to which we can bind         dgProjects.DataSource = _resource.Assignments;         if(IsPostBack)           dgProjects.DataBind();         else           DataBind();       } 

If we don't have a Resource object, we skip the data binding and the form will be displayed with blank fields so the user can fill them in.

Security

We also need to do some security checks as the form is loaded. Add this to the bottom of the Page_Load() method, as follows:

  // set security       if(!User.IsInRole("Supervisor") &&         !User.IsInRole("ProjectManager"))       {         btnNewResource.Visible = false;         btnSave.Visible = false;         btnAssign.Visible = false;         dgProjects.Columns[4].Visible = false;       }     }  

In order to edit a Resource object, the user must be either a Supervisor or a ProjectManager . If he's not, we hide the controls that enable the user to save a Resource object. Of course, the user could bypass this by typing a URL directly into his browser, but in that case our business objects themselves would enforce the security constraints, and the user would still be unable to perform invalid actions.

Saving the Data

If the user clicks the Save button, we need to save the object. First though, we need to copy the data from the form into the object. As before, we can put this functionality into a helper method, as follows:

  private void SaveFormToObject()     {       _resource.FirstName = txtFirstname.Text;       _resource.LastName = txtLastname.Text;     }  

This is fundamentally the same as what we did in ProjectEdit . We simply copy the values from the form's controls into the object.

This method is called by the Save() method, which is a bit different from the one in ProjectEdit , because we need to handle the case where the user is adding a new Resource object. The primary change is that we want to leave the user on the ResourceEdit page so that she can assign the new resource to projects, if so desired.

  private void btnSave_Click(object sender, System.EventArgs e)     {       SaveFormToObject();       if(_resource.IsNew)       {         _resource = (Resource)_resource.Save();         Session["Resource"] = _resource;         Response.Redirect("ResourceEdit.aspx");       }       else       {         _resource.Save();         Session.Remove("Resource");         Response.Redirect("Resources.aspx");       }     }  

If _resource is a new object, we save it, put it into Session , and then navigate right back to ResourceEdit . By doing this, the user is left on the ResourceEdit page, with the object loaded into the form for editing. This allows the user to change the object's data, and more importantly, assign the resource to projects.

On the other hand, if _resource isn't new, then we do the same thing as we did in ProjectEdit . We save the object, remove it from Session to save memory, and navigate back to the Resources form.

Canceling the Edit

If the user clicks the Cancel button, we return him to the Resources form, as follows:

  private void btnCancel_Click(object sender, System.EventArgs e)     {       Session.Remove("Resource");       Response.Redirect("Resources.aspx");     }  

As in the Save button handler, we're removing the business object from Session before we leave the edit form, thereby minimizing the memory required on the server and the bandwidth used by Session if it's being stored on a state server.

Selecting an Assignment

The user has the option of clicking a project to which the resource is assigned. In this case, we'll bring up the ProjectEdit page so that she can interact with the project. Before we leave this page, however, we'll save the Resource object (assuming that the user is in the right role) as follows:

  private void dgProjects_SelectedIndexChanged(        object sender, System.EventArgs e)     {       // check security       if(User.IsInRole("Supervisor")           User.IsInRole("ProjectManager"))       {         // only do save if user is in a valid role         SaveFormToObject();         _resource = (Resource)_resource.Save();       }       Session.Remove("Resource");       Guid id = new Guid(dgProjects.SelectedItem.Cells[0].Text);       Session["Project"] = Project.GetProject(id);       Response.Redirect("ProjectEdit.aspx");     }  

This is the same basic functionality that we provided in ProjectEdit .

Changing the Role

As in ProjectEdit , when the user clicks an item in the Role column of the grid, we navigate to the ChooseRole form. Before we navigate there, we get the selected ResourceAssignment object and put it into Session so that the code in ChooseRole knows which object to edit, as shown here:

  private void dgProjects_ItemCommand(object source,       System.Web.UI.WebControls.DataGridCommandEventArgs e)     {       if(e.CommandName == "SelectRole")       {         Guid id = new Guid(e.Item.Cells[0].Text);         Session["ResourceAssignment"] = _resource.Assignments[id];         Session["Source"] = "ResourceEdit.aspx";         Response.Redirect("ChooseRole.aspx");       }     }  

This code is similar to that in ProjectEdit , though we're using a ResourceAssignment object here instead of a ProjectResource object.

Removing an Assignment

When the user clicks the Remove button in the DataGrid , we need to remove the specified assignment, as shown here:

  private void dgProjects_DeleteCommand(object source,       System.Web.UI.WebControls.DataGridCommandEventArgs e)     {       string id = e.Item.Cells[0].Text;       _resource.Assignments.Remove(new Guid(id));       // rebind grid to update display       dgProjects.DataSource = _resource.Assignments;       dgProjects.DataBind();     }  

At this stage, you've seen enough code like this to know exactly what's going on here!

Adding an Assignment

Finally, we need to support the concept of assigning this resource to a new project. We'll create a dialog form named AssignToProject in which the user can choose the project and role. Here, we simply need to navigate to that form.

  private void btnAssign_Click(object sender, System.EventArgs e)     {       SaveFormToObject();       Response.Redirect("AssignToProject.aspx");     }  

Before navigating away, we need to make sure that we copy the values from our form into the business object, or they'll be lost. Remember that Web Forms data binding is read-only, so it's up to us to copy the values from the form's controls back into the object.

You should now be able to view and edit Resource objects (but you can't yet assign a resource to a project ”we'll do that next). The result is illustrated in Figure 9-28.

image from book
Figure 9-28: Example output from the ResourceEdit.aspx page

The previous image shows the form being used to edit an existing resource.

The AssignToProject Form

The final form in our Web Forms UI allows the user to assign a resource to a project (see Figure 9-29). The user needs to pick the role for the resource, and then click the project. This is virtually identical to the AssignResource form that we created earlier, but here the user selects a project instead of a resource.

image from book
Figure 9-29: Layout of the AssignToProject.aspx web form

The controls are listed in Table 9-21.

Table 9-21: Controls on the AssignToProject.aspx Page

Control

Properties

ListBox

ID=lstRoles

DataGrid

ID=dgProjects ; EnableViewState=false

Button

ID=btnCancel ; Text=Cancel

Like our other DataGrid controls, this one requires a bit more configuration. Use the Property Builder dialog box to turn off autogeneration of columns and then add the following columns listed in Table 9-22 manually.

Table 9-22: Table Columns for the dgProjects Control

Column Type

Properties

Bound

Header text=Project ID; Data Field=I D ; Visible=false

Button

Header text=Project name; Text field=Name;
Command name=Select

We'll use the DataGrid to display a list of projects from the ProjectList business object. If the user clicks one of the names, we'll use the SelectedIndexChanged event to indicate that the user wants to assign the resource to that project.

When the form is loaded, we check the security to see if the user should be here. If she's in the right role, we then populate the ListBox with the list of roles, and the DataGrid with the list of projects, as shown here:

 private void Page_Load(object sender, System.EventArgs e)     {  // check security       if(!User.IsInRole("Supervisor") &&         !User.IsInRole("ProjectManager"))       {         // they should not be here         Response.Redirect("ResourceEdit.aspx");       }       if(!Page.IsPostBack)       {         foreach(string role in Assignment.Roles)           lstRoles.Items.Add(Assignment.Roles[role]);         // set the default role to the first in the list         if(lstRoles.Items.Count > 0) lstRoles.SelectedIndex = 0;       }       dgProjects.DataSource = ProjectList.GetProjectList();       dgProjects.DataBind();  } 

When the user clicks a project in the list, we'll create a new assignment for the Resource object based on the selected role and project, as follows:

 private void dgProjects_SelectedIndexChanged(  object sender, System.EventArgs e)     {       Resource resource = (Resource)Session["Resource"];       Guid id = new Guid(dgProjects.SelectedItem.Cells[0].Text);       // TODO: this line only works in 1.1,       // so is replaced with next line for 1.0       //resource.Assignments.AssignTo(id, lstRoles.SelectedValue)       resource.Assignments.AssignTo(id,         lstRoles.SelectedItem.Value);       Response.Redirect("ResourceEdit.aspx");     }  

Finally, if the user clicks the Cancel button, we'll simply return him to the ResourceEdit form, as follows:

  private void btnCancel_Click(object sender, System.EventArgs e)     {       Response.Redirect("ResourceEdit.aspx");     }  

At this point, we should be able to assign the current resource to a project as shown in Figure 9-30.

image from book
Figure 9-30: Example output from the AssignToProject.aspx page

This completes the Web Forms UI. You should now be able to navigate throughout the UI and add, edit, and remove both Project and Resource objects.



Expert C# Business Objects
Expert C# 2008 Business Objects
ISBN: 1430210192
EAN: 2147483647
Year: 2006
Pages: 111

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net