1. Trang chủ
  2. » Công Nghệ Thông Tin

Apress Expert C sharp 2005 (Phần 11) potx

50 342 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 50
Dung lượng 1,21 MB

Nội dung

protected internal virtual void OnCurrentPrincipalChanged( object sender, EventArgs e) { if (CurrentPrincipalChanged != null) CurrentPrincipalChanged(sender, e); } I t is both v irtual a nd raises a C urrentPrincipalChanged e vent, declared as follows: public event EventHandler CurrentPrincipalChanged; If the developer of a user control needs to respond to a login/logout event, they can either override OnCurrentPrincipalChanged() or handle the CurrentPrincipalChanged event. Either way, they’ll be notified that the CurrentPrincipal property of the Thread object has changed. MainForm The MainForm form is the core of the application in that it provides the menu and status bar, and hosts the user controls for display to the user. It coordinates the flow of the entire application. Figure 9-4 shows the layout of MainForm. The R esources menu has three items comparable to those in the Projects menu, while the Admin menu has a single item: Edit Roles. The code behind each of these menu items will be discussed later in the chapter as the business functionality is implemented. F or no w , I want to focus on hosting the user controls, the Documents menu, the status bar, and the Login button. Hosting the User Controls What isn ’ t immediately obvious from Figure 9-4 is that the main region of the form contains a Panel control. All the user controls are actually contained within this Panel control rather than MainForm CHAPTER 9 ■ WINDOWS FORMS UI474 Figure 9-4. M ainForm layout 6323_c09_final.qxd 2/26/06 10:06 PM Page 474 itself. This is done so that resizing events can be handled more easily, and the overall hosting process can be simplified. The Panel control’s Dock property is set to Fill, so it automatically fills the available space in the form, even if the form is resized. Loading/Adding User Controls When a new user control is dynamically loaded (because the user chooses to view/edit a project, resource, or role), it needs to be created, added to the host’s Controls collection, positioned, and sized to fit the client area of MainForm. The same thing happens when MainForm is resized, since all the user controls it contains need to be resized accordingly. This process is split into two parts: adding a user control and showing a user control. The rea- son for the split is that when a new user control is added, it must be displayed. But already-loaded user controls also must be displayed through the Documents menu. The AddWinPart() method adds a user control to the Panel control: private void AddWinPart(WinPart part) { part.CloseWinPart += new EventHandler(CloseWinPart); part.BackColor = toolStrip1.BackColor; Panel1.Controls.Add(part); this.DocumentsToolStripDropDownButton.Enabled = true; ShowWinPart(part); } Remember that all user controls will inherit from the WinPart base contr ol—hence the naming of the AddWinPart() method and the type of the parameter. The CloseWinPart() method is hooked to handle the user control’s CloseWinPart event. I’ll dis- cuss this method shortly—but for now, you should know that its purpose is to properly remove the user control from MainForm. The user control’s BackColor property is set to match the color scheme of MainForm. Then, the user control is added to the Controls collection of the panel. This effectively adds the user control to the form. Then ShowWinPart() is called to display the user control. Finally, the Documents menu option is enabled. At this point, it’s known that there’s at least one user control hosted by MainForm, so the Documents menu should be available to the user. The ShowWinPart() method makes sure that the user control is properly positioned and sized; then it makes it visible: private void ShowWinPart(WinPart part) { part.Dock = DockStyle.Fill; part.Visible = true; part.BringToFront(); this.Text = "Project Tracker - " + part.ToString(); } Remember that the Panel control’s Dock property is set to Fill, so the Panel control automati- cally fills the available space—even when MainForm is resized. The user control is contained within the Panel control and its Dock property is also set to Fill. This means that the user control is auto- matically resized along with the Panel control, so it always fills the client area of MainForm. N ext, the user contr ol is made visible and is br ought to the fr ont: its z-or der is set so that the user control is on top of all other controls in the Panel control. These two steps ensure that the user control is visible and active. Finally, the caption text of MainForm itself is changed to r eflect the ToString() v alue of the newly active user contr ol. I f y ou look back at F igur es 9-2 and 9-3, y ou’ll notice that MainForm displays the CHAPTER 9 ■ WINDOWS FORMS UI 475 6323_c09_final.qxd 2/26/06 10:06 PM Page 475 name of the Project object being edited. You’ll see how this flows from the ToString() value of the user control later in the chapter. Removing User Controls R ecall how the A ddWinPart() m ethod sets up the C loseWinPart() m ethod to handle the user control’s CloseWinPart event. That event is raised by the user control when it is closed, and MainForm uses the event to properly remove the user control from the Panel control’s Controls collection: p rivate void CloseWinPart(object sender, EventArgs e) { WinPart part = (WinPart)sender; part.CloseWinPart -= new EventHandler(CloseWinPart); part.Visible = false; Panel1.Controls.Remove(part); part.Dispose(); if (DocumentCount == 0) { this.DocumentsToolStripDropDownButton.Enabled = false; this.Text = "Project Tracker"; } else { // Find the first WinPart control and set // the main form's Text property accordingly. // This works because the first WinPart // is the active one. foreach (Control ctl in Panel1.Controls) { if (ctl is WinPart) { this.Text = "Project Tracker - " + ((WinPart)ctl).ToString(); break; } } } } When a user control is removed, other work is required as well. The user control’s Dispose() method is called and the caption text on MainForm is reset (because there’s almost certainly a new activ e user contr ol no w). I f there’s no longer an active user control, then the caption text is set accordingly. Also notice that the CloseWinPart event is unhooked. This is an important step, because handling an event sets up an object reference behind the scenes, and failing to unhook events can cause memor y leaks (b y keeping objects in memor y when they ar e no longer needed). Resizing User Controls When MainForm is resized, the Panel control’s Resize event is automatically raised. The following code handles that event to resize all the hosted user controls: private void Panel1_Resize(object sender, EventArgs e) { foreach (Control ctl in Panel1.Controls) if (ctl is WinPart) ctl.Size = Panel1.ClientSize; } CHAPTER 9 ■ WINDOWS FORMS UI476 6323_c09_final.qxd 2/26/06 10:06 PM Page 476 With the ability to add, remove, and resize user controls, the code in MainForm covers most of the capabilities required. Of course, there’s the implementation of the Documents menu itself to consider. Documents Menu The Documents menu is a drop-down menu listing all the active documents (user controls) currently hosted by the main form. If there are no active user controls, then the menu is disabled. When the u ser selects an item from the list, that particular user control becomes the active user control. The DropDownOpening event is raised when the user clicks the Documents menu option to open the list. Handling this event allows the code to populate the list before it is displayed to the user: private void DocumentsToolStripDropDownButton_DropDownOpening( object sender, EventArgs e) { ToolStripItemCollection items = DocumentsToolStripDropDownButton.DropDownItems; foreach (ToolStripItem item in items) item.Click -= new EventHandler(DocumentClick); items.Clear(); foreach (Control ctl in Panel1.Controls) if (ctl is WinPart) { ToolStripItem item = new ToolStripMenuItem(); item.Text = ((WinPart)ctl).ToString(); item.Tag = ctl; item.Click += new EventHandler(DocumentClick); items.Add(item); } } Remember that the menu item is only enabled if there are one or more items in the Controls collection of the Panel control. Notice that a reference to each user control is put into the Tag prop- erty of the corresponding ToolStripMenuItem object. If the user clicks an item in the list, a Click event is raised and handled to make the selected user control the active control: private void DocumentClick(object sender, EventArgs e) { WinPart ctl = (WinPart)((ToolStripItem)sender).Tag; ShowWinPart(ctl); } The Tag pr operty of the menu item references the user control associated with that item, so this code needs only to cast the Tag value and make the control visible by calling the ShowWinPart() method discussed earlier. This wraps up the code in MainForm that deals with the user controls and the Documents menu. N ow let’s see how the status bar display and mouse cursor changes are handled. Status Bar MainForm has a StatusStrip control at the bottom, so the user can be informed about any long- running activity that is occurring. Also, when a long-running activity is going on, the mouse cursor should be changed to indicate that the application is busy. CHAPTER 9 ■ WINDOWS FORMS UI 477 6323_c09_final.qxd 2/26/06 10:06 PM Page 477 An easy way to handle this is to create an object that implements IDisposable. This object would update both the status display and mouse cursor, and then reset them when it is disposed. The result is that anywhere in the UI, code can be written like this: using (StatusBusy busy = new StatusBusy("Working…")) { // do long-running task here } When the object is created, it sets the status display on MainForm, and it resets the text when it is disposed. Similarly, when the object is created, it sets the mouse cursor to a busy cursor, and resets it when disposed. To do this, it needs to be able to access the MainForm object. Exposing the MainForm instance is accomplished by adding a static property named Instance to the MainForm class: private static MainForm _main; internal static MainForm Instance { get { return _main; } } The form’s constructor sets the _main field to a reference of the form: _main = this; This way, the MainForm object can be used by any code in the UI, including the StatusBusy class: public class StatusBusy : IDisposable { private string _oldStatus; private Cursor _oldCursor; public StatusBusy(string statusText) { _oldStatus = MainForm.Instance.StatusLabel.Text; MainForm.Instance.StatusLabel.Text = statusText; _oldCursor = MainForm.Instance.Cursor; MainForm.Instance.Cursor = Cursors.WaitCursor; } // IDisposable private bool _disposedValue = false; // To detect redundant calls protected void Dispose(bool disposing) { if (!_disposedValue) if (disposing) { MainForm.Instance.StatusLabel.Text = _oldStatus; MainForm.Instance.Cursor = _oldCursor; } _disposedValue = true; } CHAPTER 9 ■ WINDOWS FORMS UI478 6323_c09_final.qxd 2/26/06 10:06 PM Page 478 public void Dispose() { // Do not change this code. // Put cleanup code in Dispose(bool disposing) above Dispose(true); GC.SuppressFinalize(this); } } When a StatusBusy object is created, it sets the status text and mouse cursor, storing the old values for later use: _oldStatus = MainForm.Instance.StatusLabel.Text; MainForm.Instance.StatusLabel.Text = statusText; _oldCursor = MainForm.Instance.Cursor; MainForm.Instance.Cursor = Cursors.WaitCursor; Then, when the object is disposed, the status text and cursor are reset to their previous values: MainForm.Instance.StatusLabel.Text = _oldStatus; MainForm.Instance.Cursor = _oldCursor; This is one of the simplest ways to implement powerful status notification and cursor handling for the user in a Windows Forms UI. Login Button The final bit of common functionality implemented in MainForm allows the user to log into or out of the application. It is important to realize that the ProjectTracker application allows unauthorized or guest users to view certain data, and so the user can interact with the application even if they haven’t logged in. The login process is triggered when the application first loads, and when the user clicks the Login button on the menu. In both cases, a DoLogin() method is called to handle the actual login/ logout behavior: private void DoLogin() { ProjectTracker.Library.Security.PTPrincipal.Logout(); if (this.LoginToolStripButton.Text == "Login") { LoginForm loginForm = new LoginForm(); loginForm.ShowDialog(this); } System.Security.Principal.IPrincipal user = Csla.ApplicationContext.User; if (user.Identity.IsAuthenticated) { this.LoginToolStripLabel.Text = "Logged in as " + user.Identity.Name; this.LoginToolStripButton.Text = "Logout"; } else { this.LoginToolStripLabel.Text = "Not logged in"; this.LoginToolStripButton.Text = "Login"; } CHAPTER 9 ■ WINDOWS FORMS UI 479 6323_c09_final.qxd 2/26/06 10:06 PM Page 479 // reset menus, etc. ApplyAuthorizationRules(); // notify all documents foreach (Control ctl in Panel1.Controls) if (ctl is WinPart) ((WinPart)ctl).OnCurrentPrincipalChanged(this, EventArgs.Empty); } Before doing anything else, this method ensures that the CurrentPrincipal property of the Thread is set to an unauthenticated PTPrincipal object: ProjectTracker.Library.Security.PTPrincipal.Logout(); This way, if the user’s credentials are invalid, she can at least use the application as an unauthen- ticated user. Recall that the data portal requires that the principal object inherit from Csla.Security. BusinessPrincipalBase . PTPrincipal meets this requirement, and so the current principal is set to an unauthenticated PTPrincipal object by calling the Logout() method. Next, the text of the button on the menu is checked. If the text is Login, then a login process is initiated. The login process is actually handled by a Login dialog form, which is shown to the user as a modal dialog. That dialog prompts the user for their credentials and calls PTPrincipal.Login() (as implemented in Chapter 8) to validate them. The result is that the CurrentPrincipal property on the Thread object will either be an authen- ticated PTPrincipal or an unauthenticated PTPrincipal. The status of the principal object is used to determine whether the user is logged in or not: if (user.Identity.IsAuthenticated) { this.LoginToolStripLabel.Text = "Logged in as " + user.Identity.Name; this.LoginToolStripButton.Text = "Logout"; } else { this.LoginToolStripLabel.Text = "Not logged in"; this.LoginToolStripButton.Text = "Login"; } If the user was authenticated, then the button text is changed to Logout and the user’s name is displayed in the menu. Otherwise, the button text is changed to Login, and text indicating that the user isn’t logged in is displayed. I n any case , an ApplyAuthorizationRules() method is called so that MainForm can update its display based on the user’s identity (or lack thereof). Then all the active user controls are notified that the principal has changed: // reset menus, etc. ApplyAuthorizationRules(); // notify all documents foreach (Control ctl in Panel1.Controls) if (ctl is WinPart) ((WinPart)ctl).OnCurrentPrincipalChanged(this, EventArgs.Empty); Each user control is responsible for handling this event and responding appropriately. Recall that the WinPart base control implements the OnCurrentPrincipalChanged() method and subsequently raises a protected ev ent to the code in the user control. CHAPTER 9 ■ WINDOWS FORMS UI480 6323_c09_final.qxd 2/26/06 10:06 PM Page 480 The ApplyAuthorizationRules() method in MainForm is responsible for enabling and disabling menu items. This method is somewhat long and repetitive, so I won’t show the whole thing, but here’s the code to enable/disable one menu item: this.NewProjectToolStripMenuItem.Enabled = Project.CanAddObject(); Notice how the actual authorization check is delegated to the static method of the Project business class. These methods were discussed in Chapter 8, and were implemented specifically to enable scenarios like this. The idea is that MainForm has no idea whether particular users or roles are authorized to add Project objects. Instead, the Project class itself has that knowledge, and MainForm simply asks Project whether the current user is authorized. The end result is good separation of concerns: Project is concerned with whether users can and can’t add objects, while MainForm is concerned with the UI details of enabling and disabling controls. Login Form The DoLogin() method in MainForm calls a Login dialog form to collect and authenticate the user’s credentials. After gathering credentials from the user, this dialog form will call PTPrincipal.Login() to do the authentication itself. Figure 9-5 shows the Login form layout. All the wor k occurs when OK is clicked. At that point, the credentials entered by the user are v erified: private void OK_Click(object sender, EventArgs e) { using (StatusBusy busy = new StatusBusy("Verifying credentials ")) { ProjectTracker.Library.Security.PTPrincipal.Login( this.UsernameTextBox.Text, this.PasswordTextBox.Text); } this.Close(); } Notice the use of the StatusBusy object to update the status text and mouse cursor. Also notice the simplicity of this code. Since PTPrincipal.Login() does all the work of authenticating the user, there’s just not much work to do in the UI. This is a theme you’ll see throughout the rest of the chapter . CHAPTER 9 ■ WINDOWS FORMS UI 481 Figure 9-5. Layout of the Login form 6323_c09_final.qxd 2/26/06 10:06 PM Page 481 Using Windows Integrated Security If you wanted to use Windows integrated security, you wouldn’t need a login form because the client workstation already knows the user’s identity. Instead, you would need to add a bit of code to MainForm so that as it loads, the CurrentPrincipal is configured with a WindowsPrincipal object. The following code shows how to detect the authentication mode and adapt to use either Windows or custom authentication appropriately: private void MainForm_Load(object sender, EventArgs e) { if (Csla.ApplicationContext.AuthenticationType == "Windows") AppDomain.CurrentDomain.SetPrincipalPolicy( System.Security.Principal.PrincipalPolicy.WindowsPrincipal); else DoLogin(); if (DocumentCount == 0) this.DocumentsToolStripDropDownButton.Enabled = false; ApplyAuthorizationRules(); } Calling SetPrincipalPolicy() to set the WindowsPrincipal option tells the .NET runtime to return the current WindowsPrincipal object for the CurrentPrincipal property of the Thread. ■Note If you use Windows integrated security, and you are using a remote data portal, you must make sure to change the server configuration file to also use Windows security. If the data portal is hosted in IIS, the virtual root must be set to disallow anonymous access, thereby forcing the client to provide IIS with the Windows identity from the client workstation via integrated security. Business Functionality With the common functionality in MainForm, WinPart, StatusBusy, and Login covered, we can move on to the business functionality itself. As I mentioned earlier, I’ ll walk through the RolesEdit user control, the ProjectSelect dialog, and the ProjectEdit user control in some detail. ResourceSelect and ResourcEdit are available in the download and follow the same implementation approach. All of these forms and user controls will be created using the new data binding capabilities built into V isual S tudio 2005. These capabilities allo w the UI developer to literally drag-and-drop business classes or properties onto the form to create the controls and set up data binding. The developer pro- ductivity gained through this approach is simply amazing. The detail edit forms ( ProjectEdit and ResourceEdit) will also make use of the ReadWriteAuthorization and BindingSourceRefresh contr ols cr eated in Chapter 5, as w ell as the standar d Windows Forms ErrorProvider contr ol. All thr ee controls are e xtender contr ols , adding important extra capabilities to the other controls on each form or user control. Let’s start by looking at the business code in MainForm that displays the other forms and user controls. MainForm You’ve already seen the code in MainForm that exists to provide common functionality around the user controls, authentication, and authorization. But the form also implements the menu options to add, edit, and delete project and resource data, and to edit the list of roles. CHAPTER 9 ■ WINDOWS FORMS UI482 6323_c09_final.qxd 2/26/06 10:06 PM Page 482 Displaying User Controls Thanks to the common code discussed earlier, none of these menu options are difficult to imple- ment. For instance, when the user chooses the menu option to edit the list of roles, the code simply checks to see if the RolesEdit user control is already loaded. If it is, the existing user control is made active; otherwise, a new one is created and displayed: private void EditRolesToolStripMenuItem_Click( object sender, EventArgs e) { // see if this form is already loaded foreach (Control ctl in Panel1.Controls) { if (ctl is RolesEdit) { ShowWinPart((WinPart)ctl); return; } } // it wasn't already loaded, so show it. AddWinPart(new RolesEdit()); } A slightly more complex variation occurs when the user clicks the menu to add a project or resource. In both cases, a new instance of the appropriate business object is created and is passed to a new instance of the appropriate user control. For example, when the user opts to add a new project, this code is run: private void NewProjectToolStripMenuItem_Click( object sender, EventArgs e) { using (StatusBusy busy = new StatusBusy("Creating project ")) { AddWinPart(new ProjectEdit(Project.NewProject())); } } Project.NewProject() is called to create the new Project object, and it is then passed to the con- structor of a ProjectEdit user control. That user control, now populated with data from the Project object, is then added to the list of activ e user controls and is displayed. Editing an Existing Object Even more complex is the process of editing an existing project or resource. This is because in both cases , the user must be pr ompted to select the specific item to edit. The ProjectSelect and ResourceSelect dialog forms are used to prompt the user for the particular object they wish to edit. Here’s the code behind the menu option to edit a resource: private void EditResourceToolStripMenuItem_Click( object sender, EventArgs e) { ResourceSelect dlg = new ResourceSelect(); dlg.Text = "Edit Resource"; if (dlg.ShowDialog() == DialogResult.OK) CHAPTER 9 ■ WINDOWS FORMS UI 483 6323_c09_final.qxd 2/26/06 10:06 PM Page 483 [...]... value for the project that the user selected from the list Selecting a Project When ProjectSelect is displayed, it presents the user with a list of projects At this point, the user can either select one and click OK, or click Cancel Either way, the DialogResult property is set and the form is closed For instance, here’s the code for the Cancel button: private void Cancel_Button_Click(object sender, EventArgs... Project node represents the child objects It is that node that you drag onto the designer to create the DataGridView control and set up the data binding Notice that a resourcesBindingSource control is automatically added in the component tray, and the DataGridView control is bound to that new BindingSource control Each business object must have its own BindingSource control because each BindingSource control... hard-to-debug problems in your application In this case, the form is closed immediately upon successfully saving the data, so the UI is not rebound Instead, the user control’s Close() method is called: this.Close(); The Save() call and closing of the user control (or rebinding of the BindingSource control) occurs in a try block If an exception occurs during the Save() call, the _roles field will not be... MessageBoxIcon.Exclamation); } finally { this.rolesBindingSource.RaiseListChangedEvents = true; } } Notice that the Clone() method call is gone, and the original object in _roles is saved directly If this succeeds without an exception, a newly updated copy of the object is returned from Save(), and the BindingSource controls are rebound to that new object But if an exception does occur, then no new object... DataSource property of roleListBindingSource is set to a RoleList object If this is the first time GetList() has been called, this will incur a call to the database; otherwise, the collection is returned from the cache implemented in Chapter 8 The DataSource property of projectBindingList is set to the Project object passed into the constructor as a parameter By accepting this object as a parameter, ProjectEdit... this.projectBindingSource.RaiseListChangedEvents = false; this.resourcesBindingSource.RaiseListChangedEvents = false; // do the save Project temp = _project.Clone(); temp.ApplyEdit(); try { _project = temp.Save(); _project.BeginEdit(); if (rebind) { // rebind the UI this.projectBindingSource.DataSource = null; this.resourcesBindingSource.DataSource = null; this.projectBindingSource.DataSource = _project;... next to ProjectList, Resource, and the other object nodes This change occurred because I chose the Details option from the menu for the Project node When an object is set to use details mode, the individual control types of the properties for that object come into play When the object is dragged onto the designer, controls for each property will be created In fact, a pair of controls is created: a Label... business object: Roles temp = _roles.Clone(); This is easily done, since all CSLA NET business objects automatically support the Clone() method Remember that this method copies the object and all child objects it contains In this case, it copies the Roles object and all the Role objects in the collection Then the copy of the object is saved: _roles = temp.Save(); Notice that the result of the Save() method... undo capability discussed in Chapter 3 This method call tells the object to take a snapshot of its current state, so the object can be restored to this exact state later if the user clicks the Cancel or Close button without saving the data first With that done, data binding is initialized: this.roleListBindingSource.DataSource = RoleList.GetList(); this.projectBindingSource.DataSource = _project; The... object, and the second sets the data source using that object Setting the DataSource property of the BindingSource control automatically connects all the data bound controls on the form to the underlying data source object The result is that data in the object is displayed in the controls and is available for editing by the user Of course, the exception-handling code is important too If an exception occurs . ProjectEdit(Project.NewProject())); } } Project.NewProject() is called to create the new Project object, and it is then passed to the con- structor of a ProjectEdit user control. That user control,. control, because it can be directly bound to an editable root collection object such as ProjectTracker.Library.Roles. Using a Business Class as a Data Source To bind controls to an object, choose. principal has changed: // reset menus, etc. ApplyAuthorizationRules(); // notify all documents foreach (Control ctl in Panel1.Controls) if (ctl is WinPart) ((WinPart)ctl).OnCurrentPrincipalChanged(this,

Ngày đăng: 06/07/2014, 00:20