Wrox Professional Web Parts and Custom Controls with ASP.NET 2.0 phần 9 ppsx

45 360 0
Wrox Professional Web Parts and Custom Controls with ASP.NET 2.0 phần 9 ppsx

Đ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

Working with the Web Part Architecture Because this book is focused on creating controls, not a lot of time has been spent on how to use controls. As an experienced ASP.NET developer, you are already familiar with how to use the vari- ous ASP.NET server controls. However, Web Parts present a different issue. Not only are Web Parts the newest part of the ASP.NET toolkit, the way they work together and their dependence on the ASP.NET personalization sub-system make working with Web Parts a different experience than working with other ASP.NET controls. Chapter 2 described how to design pages with Web Parts and how users interact with them. This chapter describes how a programmer interacts with Web Parts to: ❑ Control which personalization provider is to be used ❑ Set whether changes are applied to the current user or all users ❑ Set and determine which types of changes are permitted on a page ❑ Implement authorization strategies for your Web Parts by creating a custom WebPartManager ❑ Monitor and manage personalization changes made by the user by interacting with WebPartManager events ❑ Dynamically convert standard ASP.NET controls on the page to Web Parts and add them to WebPartZones ❑ Make personalization changes to the host page from the host page’s code ❑ Import and export personalization settings and support importing/exporting a WebPart that you create With one exception, none of the material in this chapter directly discusses how to create a control (the one exception is the section on setting attributes to enable exporting for a Web Part). However, 17_57860x ch11.qxd 10/4/05 9:32 PM Page 353 the more you know about how developers will expect to use your Web Part, the better job you will do of designing it. And, of course, it’s not unlikely that in addition to building Web Parts, you want to use them yourself. Setting Personalization Options on the WebPartManager In this section, you learn how to: ❑ Control the personalization options in the WebPartManager ❑ Have changes made by one user shared by many users ❑ Implement authorization for Web Parts Controlling WebPartManager Personalization Options You can control much of how personalization is handled by working with the ASP.NET Personalization object, which can be retrieved from the WebPartManager’s Personalization property. The methods and properties on this object let you manage the way that personalization is handled on the page: ❑ Switching personalization providers: You can change the personalization provider that is being used by a page by setting the ProviderName property of the Personalization object (set- ting up personalization providers was discussed in Chapter 7). This Visual Basic 2005 code sets the WebPartManager to use the Access provider: Me.WebPartManager1.Personalization.ProviderName = _ “AspNetAccessPersonalizationProvider” In C#: this.WebPartManager1.Personalization.ProviderName = “AspNetAccessPersonalizationProvider”;. ❑ Discarding personalization changes: You can return a page to its original state by calling the ResetPersonalizationState method. Before calling this method, you can determine if there are any changes to be backed out by checking the Personalization object’s HasPersonalizationState property, which is True if the page has been personalized. ❑ Ensuring that changes are allowed: The Personalization object’s EnsureEnabled will be True when the personalization infrastructure is fully enabled and ready to accept changes for the cur- rent user. Setting the Personalization object’s Enabled property to False prevents personalization changes from being made by the current user. The IsModifiable property allows you to check whether the current user is allowed to make personalization changes. You can also disable any personalization changes from being made to a page by setting the WebPartManager’s Enable property to False. 354 Chapter 11 17_57860x ch11.qxd 10/4/05 9:32 PM Page 354 Applying Changes to Other Users Personalization changes are made in one of two scopes: shared or user. When the scope is set to user (the default), the changes made by a user affect that page only when it is requested by that user. To put it another way: in user scope, a user’s personalization changes are visible to that user only. When the scope is set to shared, however, changes made to the page are made for all users. You control the scope of a change by calling the ToggleScope method of the Personalization object. Because the ToggleScope method switches the scope from whatever its current state is to the other state, you will usually want to determine the current scope before calling ToggleScope. The current scope can be determined by testing the Personalization object’s Scope property against one of the enumerated PersonalizationScope values. Because not all users are allowed to make changes in shared mode, you should also check the Personalization object’s CanEnterSharedScope property before calling the ToggleScope method. CanEnterSharedScope returns True if the user is allowed to make shared changes (or if there is some other reason that shared changes can’t be made). This Visual Basic 2005 code puts all of these together: Dim prs As System.Web.UI.WebControls.WebParts.WebPartPersonalization Dim prs As UI.WebControls.WebParts.WebPartPersonalization; prs = Me.WebPartManager1.Personalization If prs.CanEnterSharedScope = True Then If prs.Scope = PersonalizationScope.User Then prs.ToggleScope() End If End If In C#: UI.WebControls.WebParts.WebPartPersonalization prs; prs = this.WebPartManager1.Personalization; if(prs.CanEnterSharedScope == true) { if(prs.Scope == PersonalizationScope.User) { prs.ToggleScope(); } } If you do change the WebPartManager’s scope, you can determine the original scope for the WebPartManager by reading the Personalization object’s InitialState property. Implementing Authorization Every Web Part has an AuthorizationFilter property that can be set to any string value. If you want to take advantage of this property, you must create your own WebPartManager and override either its OnAuthorizeWebPart or IsAuthorized method. In these methods, you can add code to check the AuthorizationFilter property on Web Parts and prevent Web Parts from being displayed. These methods are called automatically, as Web Parts are associated with the WebPartManager on the page. 355 Working with the Web Part Architecture 17_57860x ch11.qxd 10/4/05 9:32 PM Page 355 The following example is a Visual Basic 2005 class that inherits from WebPartManager and overrides the OnAuthorizeWebPart method. The e parameter passed to the OnAuthorizeWebPart method references the Web Part being authorized through the WebPart property. You indicate that the Web Part is not authorized by setting the e parameter’s IsAuthorized property to False: Public Class PHVWebPartManager Inherits System.Web.UI.WebControls.WebParts.WebPartManager Protected Overrides Sub OnAuthorizeWebPart( _ ByVal e As System.Web.UI.WebControls.WebParts.WebPartAuthorizationEventArgs) If e.AuthorizationFilter <> “Created by PH&V” Then e.IsAuthorized = False End If MyBase.OnAuthorizeWebPart(e) End Sub End Class In C#: public class PHVWebPartManager : System.Web.UI.WebControls.WebParts.WebPartManager { protected override void OnAuthorizeWebPart( System.Web.UI.WebControls.WebParts.WebPartAuthorizationEventArgs e) { if(e.AuthorizationFilter != “Created by PH&V”) { e.IsAuthorized = false; } base.OnAuthorizeWebPart(e); } } While the OnAuthorizeWebPart currently performs no functions, it’s a good practice to continue to call the underlying method in case later versions of ASP.NET do implement some default authorization functionality. The IsAuthorized method calls the OnAuthorizeWebPart method (unless IsAuthorized has been over- ridden), so overriding OnAuthorizeWebPart effectively overrides IsAuthorized. However, if you prefer to override IsAuthorized, the method is passed four parameters: ❑ The type of the Web Part ❑ The path to the Web Part ❑ The Web Part’s AuthorizationFilter ❑ A Boolean isShared parameter that is set to True if the Web Part has its personalization changes shared among users 356 Chapter 11 17_57860x ch11.qxd 10/4/05 9:32 PM Page 356 In the IsAuthorized method, if the Web Part fails the test, you must return False from the IsAuthorized method: Public Class PHVWebPartManager Inherits System.Web.UI.WebControls.WebParts.WebPartManager Public Overrides Function IsAuthorized(ByVal type As System.Type, _ ByVal path As String, ByVal authorizationFilter As String, _ ByVal isShared As Boolean) As Boolean If authorizationFilter <> “Created by PH&V” Then Return False End If End Function End Class In C#: public class PHVWebPartManager : System.Web.UI.WebControls.WebParts.WebPartManager { public override bool IsAuthorized(System.Type type, string path, string authorizationFilter, bool isShared) { if(authorizationFilter != “Created by PH&V”) { return false; } else { return true; } In order to prevent the AuthorizationFilter from being reset by code on the host page, you need to over- ride your control’s AuthorizationFilter and set it to a constant value. Some history: The AuthorizationFilter property replaces an earlier Roles property in the first Beta of .NET that allowed developers to set the user roles that a Web Part could be used by (for example, Admin or User). The AuthorizationFilter allows developers a more flexible approach to authorization. To duplicate the functionality of the original Roles property, for instance, the OnAuthorizeWebPart method can check the AuthorizationFilter property of the Web Part for the names of the roles that a Web Part can be used by. The OnAuthorizeWebPart can then compare those roles to the role of the currently logged on user. Managing Personalization for Web Parts Just because your users can move any Web Part to any WebPartZone doesn’t mean that you should let them — it’s your application and you need to maintain control over what customizations you permit. Nor is customization restricted to what the user can do in the browser. While up until now I’ve concen- trated on how the user can customize his page by interacting with the page in the browser, you can also customize Web Parts and their pages from your code. 357 Working with the Web Part Architecture 17_57860x ch11.qxd 10/4/05 9:32 PM Page 357 In this section you see how to: ❑ Monitor the changes that a user makes while personalizing your page ❑ Control what changes you permit your users to make ❑ Customize and personalize your page from your code Much of the work that you can do with Web Parts in your code is done by calling methods and proper- ties of the WebPartManager and interacting with the WebPartManager’s events. You’ve already seen how the DisplayMode property allows you to put the WebPartManager into a mode that allows the user to make changes. The DisplayMode must be set to some object that inherits from the WebPartDisplayMode object (you’ve seen these objects already also: WebPartManager.DesignDisplayMode, WebPartManager .CatalogDisplayMode, and so on). These objects have five properties that control what personalization is possible: ❑ AllowPageDesign: When True, indicates that the user can make changes to the page’s layout ❑ AssociatedWithToolZone: When True, indicates that there is a tool zone that must be present for this mode to be used ❑ Name: The name of the mode ❑ RequiresPersonalization: When True, indicates that this mode can be used only if Personalization is enabled for the site ❑ ShowHiddenWebParts: Causes parts that have their Hidden property set to True to be displayed For example, for the CatalogDisplayMode object, the properties have these settings: ❑ AllowPageDesign: True ❑ AssociatedWithToolZone: True ❑ Name: Catalog ❑ RequiresPersonalization: True ❑ ShowHiddenWebParts: True As an example of the settings for a display mode, the BrowseDisplayMode (the default mode that allows users to just Close and Minimize Web parts) has both its AllowPageDesign and RequiresPersonalization properties set to False. All of the properties on the DisplayMode object are read-only. Checking Whether a DisplayMode Is Supported In addition to the five properties, the various *DisplayMode objects also have an IsEnabled method that, when passed the WebPartManager for the page, returns True if personalization is supported for that DisplayMode. A user may not be permitted to make personalizations, for instance. Passing the page’s 358 Chapter 11 17_57860x ch11.qxd 10/4/05 9:32 PM Page 358 WebPartManager to the ConnectDisplayMode allows you to check to see if personalizing connections are permitted, as this Visual Basic 2005 code does before setting the WebPartManager’s DisplayMode: If WebPartManager.ConnectDisplayMode.IsEnabled(Me.WebPartManager1) = True Then Me.WebPartManager1.DisplayMode = WebPartManager.ConnectDisplayMode End If In C#: if(WebPartManager.ConnectDisplayMode.IsEnabled(this.WebPartManager1) == true) { this.WebPartManager1.DisplayMode = WebPartManager.ConnectDisplayMode; } You still need to check if the necessary controls are present on the page. For instance, while personaliz- ing connections may be permitted, if a ConnectionsZone isn’t on the page it’s not possible for connec- tions to be created. You can check to see if a DisplayMode is supported by using the WebPartManager’s SupportedDisplayModes property. You pass the name of a DisplayMode to the SupportedDisplayModes collection and, if the page supports the mode, the DisplayMode will be returned. More importantly, if the mode isn’t supported, the SupportedDisplayModes property returns Nothing in Visual Basic 2005 or null in C#. The following Visual Basic 2005 code tests for the Connect mode being supported before attempting to put the WebPartManager in connect mode by using the SupportedDisplayModes property: If Me.WebPartManager1.SupportedDisplayModes(“Connect”) IsNot Nothing Then Me.WebPartManager1.DisplayMode = WebPartManager.ConnectDisplayMode End If In C#: if(this.WebPartManager1.SupportedDisplayModes[“Connect”] != null) { this.WebPartManager1.DisplayMode = WebPartManager.ConnectDisplayMode; } Don’t confuse the SupportedDisplayModes collection with the WebPartManager’s DisplayModes collection. The DisplayModes collection lists all the display modes that the manager supports. This list is not limited to the display modes that are possi- ble for the current page. For example, while the WebPartManager supports connect mode (so ConnectDisplayMode is found in the DisplayModes collection), the page does not support connect mode if there are no ConnectionsZones on the page (and, as a result, the ConnectDisplayMode cannot be found in the SupportedDisplayModes). Presumably, the DisplayModes property is designed to support the creation of new WebPartManagers that support a different set of modes from the modes supported by the ASP.NET Framework’s WebPartManager. 359 Working with the Web Part Architecture 17_57860x ch11.qxd 10/4/05 9:32 PM Page 359 Managing Personalization Changes When making personalization changes to a page while it’s displayed in a browser, it’s easy to think that the Web Part is actually closed or moved to a new zone when the user completes the action in the browser. In reality, of course, all that the user can do in the browser is indicate what change she wants made — the actual change is made back at the server. When the user has finished closing a part, or mov- ing it to a new zone, or whatever change the user makes, the data from the page is sent back to the server and ASP.NET starts making the change to the page. The sequence of events that ASP.NET follows as the user personalizes a page, beginning when the user clicks the button that enables personalization, is: 1. The user clicks the button that puts the WebPartManager into one of the design modes. 2. The page’s data is posted back to the server for processing by ASP.NET. 3. The button’s Click event executes and the code in the event puts the WebPartManager into one of the design modes. 4. The page is returned to the user. 5. The user performs some personalization activities (for example, dragging a Web Part to another WebPartZone). 6. The page’s data is posted back to the server for processing by ASP.NET. 7. ASP.NET implements the changes made by the user in the browser (such as moving the Web Part to the new zone). After the user puts a page into one of the design modes, the personalization changes that the user makes don’t involve performing any of the traditional actions for interacting with ASP.NET (such as clicking a button or selecting items in a list box). Nor do the various user controls, custom controls, or Web Parts have any events related to personalization changes made by the user. If you want to manage the cus- tomizations made by your users, you have to use the events fired by the WebPartManager. WebPartManager Events To allow you to manage personalization for the page, the WebPartManager fires events as part of imple- menting the changes the user made in the browser. You can put code into these events to control the per- sonalization performed by the user. These events are also fired when you manage Web Parts from your code (as discussed later in this chapter), so by putting code into the WebPartManager’s events you can ensure that your personalization management code manages both changes made by the user of the page and changes made from your code. For any change to a Web Part, the WebPartManager fires two events: one event that fires before the change takes place (these events have names ending in “ing”) and one that fires after the change has taken place (these events have names ending in “ed”). For instance, when the user closes a Web Part, the WebPartClosing event fires before ASP.NET removes the Web Part from the page and the WebPartClosed event fires after ASP.NET has removed the Web Part from the page. The exceptions to this two-event rule are the events related to connecting, which include events related to the connections on the page in addition to the events related to the Web Parts being connected. Those events are discussed in Chapter 10. 360 Chapter 11 17_57860x ch11.qxd 10/4/05 9:32 PM Page 360 Managing the DisplayMode Most Web Part personalizations begin with the user clicking a button to run code that changes the WebPartManager’s DisplayMode. Two events fire as the WebPartManager’s DisplayMode changes: DisplayModeChanging and DisplayModeChanged. The Click event for the button fires after the Page’s Load event and before the Page’s LoadComplete event (as usual for client-side triggered events). The DisplayMode-related events fire while the code in the Click event executes. Setting the value of the DisplayMode to its current value does not cause any events to fire. The DisplayModeChanging and DisplayModeChanged events fire only if the WebPartManager’s DisplayMode property is set to a new value. As the code that changes the DisplayMode executes, the DisplayModeChanging event fires. When the first event (DisplayModeChanging) fires, the DisplayMode in the WebPartManager won’t yet have been changed. After that first event completes, the DisplayMode is updated to the new value set in the code. After the first event has fired, the second event (DisplayModeChanged) fires. After both of the DisplayMode* events have fired, ASP.NET executes any code that follows the line that changed the DisplayMode. In other words, the process looks like this: Visual Basic: Protected Sub btnChangeMode_Click( _ ByVal sender As Object, ByVal e As System.EventArgs) Handles btnChangeMode.Click Me.WebPartManager1.DisplayMode = WebPartManager.DesignDisplayMode ‘ WebPartManager_DisplayModeChanging event fires ‘ DisplayMode set to new value ‘ WebPartManager_DisplayModeChanged event fires Me.txtMessage.Text = “You can now modify the page.” End Sub As is the case with other .NET events, the WebPartManager’s events are passed two parameters. In the first event, the second parameter (the parameter called e) provides you with an opportunity to interrupt the change being made. In addition, the e parameter normally has other properties customized for the different personalization changes. For example, the e parameter passed to the DisplayModeChanging event has two properties that are useful for managing personalization: The DisplayModeChanging and DisplayModeChanged events also fire when the user changes the display mode as a side effect of some other action. For instance, when the user finishes working with a CatalogEditor as part of adding new parts to the page, the user closes the CatalogEditor by clicking the editor’s Close button. In addition to closing down the Catalog Editor, ASP.NET also puts the page back into BrowseDisplayMode, which causes the DisplayModeChanging and DisplayModeChanged events to fire. 361 Working with the Web Part Architecture 17_57860x ch11.qxd 10/4/05 9:32 PM Page 361 ❑ Cancel: Setting this property to True cancels the change to the DisplayMode. ❑ NewDisplayMode: This property returns the value that the DisplayMode is going to be set to. Because the DisplayMode hasn’t been changed at this point you can still retrieve the original value of the DisplayMode from the WebPartManager. As a result, in the DisplayModeChanging event you can test for combinations of the current and future display modes and suppress changes that you don’t want to support. More commonly, in the DisplayModeChanging event, these properties allow you to test for a change to a specific DisplayMode and prevent that change under some conditions (you could, for instance, prevent specific users from making some changes). Because the DisplayMode is actually set to an object you can’t use an equals sign to compare the NewDisplayMode to one of the predefined DisplayMode values. Instead you use the Equals method of the NewDisplayMode to see if it’s the same object as the DisplayMode that you’re testing for. This Visual Basic 2005 example checks to see if the user is going into catalog display mode (which lets the user add new controls to the page). If the user is making that change, the code sets the e parameter’s Cancel property to True to suppress the change: If e.NewDisplayMode.Equals(WebPartManager.CatalogDisplayMode) Then e.Cancel = True End If In C#: if(e.NewDisplayMode.Equals(WebPartManager.CatalogDisplayMode)) { e.Cancel = true; } No error is raised when a change is canceled and the DisplayModeChanged event is not fired. If you are using the DisplayModeChanged event to cancel changes under some circumstances, you need to check after any display mode change to check if the change wasn’t canceled. A full version of the code in the button’s Click event that includes this test looks like this (remember, the DisplayMode* events fire after the DisplayMode is changed and before the next line of code executes): Visual Basic: Protected Sub btnChangeMode_Click( _ ByVal sender As Object, ByVal e As System.EventArgs) Handles btnChangeMode.Click Me.WebPartManager1.DisplayMode = WebPartManager.CatalogDisplayMode If Me.WebPartManager1.DisplayMode.Equals(WebPartManager.CatalogDisplayMode) Then Me.txtMessage.Text = “You can now modify the page.” End If End Sub 362 Chapter 11 17_57860x ch11.qxd 10/4/05 9:32 PM Page 362 [...]... prevents the ExportMode from being set to WebPartExportMode.All: Public Overrides Property ExportMode() As _ System .Web. UI.WebControls.WebParts.WebPartExportMode Get Return MyBase.ExportMode End Get Set(ByVal value As System .Web. UI.WebControls.WebParts.WebPartExportMode) If value = WebControls.WebParts.WebPartExportMode.All Then MyBase.ExportMode = WebControls.WebParts.WebPartExportMode.None Else MyBase.ExportMode... Sub WebPartManager1_WebPartMoving(ByVal sender As Object, _ ByVal e As System .Web. UI.WebControls.WebParts.WebPartMovingEventArgs) _ Handles WebPartManager1.WebPartMoving If e.WebPart.ID = “SearchPart” And _ e.Zone.ID = “ListingZone” Then e.Cancel = True End If End Sub The same code in C# looks like this: protected void WebPartManager1_WebPartMoving(object sender, System .Web. UI.WebControls.WebParts.WebPartMovingEventArgs... exported only when the Web Part’s ExportMode is set to export sensitive data: _ Public Property Data() As String In C#: [Web. UI.WebControls.WebParts.Personalizable( WebControls.WebParts.PersonalizationScope.Shared, false)] public string Data To export sensitive data, the Web Part’s ExportMode must... GenericWebPart that the ASP.NET control has been wrapped in: Dim wp2 As GenericWebPart wp2 = Me.WebPartManager1.GetGenericWebPart(txt) In C#: GenericWebPart wp2; wp2 = this.WebPartManager1.GetGenericWebPart(txt); Exporting and Importing Web Parts In addition to creating Web Parts at run time from standard controls, you can also create Web Parts by importing from files of Web Part information Importing a Web. .. you can: ❑ Close Web Parts ❑ Move Web Parts from one zone to another ❑ Add Web Parts to a page ❑ Convert ASP.NET controls into Web Parts dynamically at run time ❑ Import and export Web Parts with their personalization information The other chapters in this book gave you the capability to create Web Parts This chapter, on the other hand, has given you the capability to control the Web Parts that you use... with the Web Part Architecture In C#: bool bolChanging; protected void WebPartManager1_SelectedWebPartChanging(object sender, System .Web. UI.WebControls.WebParts.WebPartCancelEventArgs e) { if(this.WebPartManager1.SelectedWebPart == null) { //The first time that a Web Part has been selected //SelectedWebPart is Nothing //e.WebPart points to the selected Web Part } else { if(e.WebPart.Title != this.WebPartManager1.SelectedWebPart.Title)... be set to True to cancel the change Manipulating Web Par ts from Code You can take advantage of Web Parts flexibility by manipulating Web Parts from your code From your code you can close Web Parts, move Web Parts between zones, connect zones, or add Web Parts to a zone You can even create new Web Parts from standard ASP.NET controls Manipulating Web Parts from code gives you two capabilities: ❑ The... WebControls.WebParts.WebPartExportMode.None Else MyBase.ExportMode = value End If End Set End Property 378 Working with the Web Part Architecture In C#: public override System .Web. UI.WebControls.WebParts.WebPartExportMode ExportMode { get { return base.ExportMode; } set { if(value == WebControls.WebParts.WebPartExportMode.All) { base.ExportMode = WebControls.WebParts.WebPartExportMode.None; } else { base.ExportMode = value; } } } Exporting... ‘SelectedWebPart points to the old part ‘e.WebPart points to the old part bolChanging = True End If End If End Sub Protected Sub WebPartManager1_SelectedWebPartChanged(ByVal sender As Object, _ ByVal e As System .Web. UI.WebControls.WebParts.WebPartEventArgs) _ Handles WebPartManager1.SelectedWebPartChanged If Me.WebPartManager1.SelectedWebPart Is Nothing Then ‘The user is changing Web Parts and this... notes what SelectedWebPart and e.WebPart are pointing to at each point: Dim bolChanging As Boolean Protected Sub WebPartManager1_SelectedWebPartChanging(ByVal sender As Object, _ ByVal e As System .Web. UI.WebControls.WebParts.WebPartCancelEventArgs) _ Handles WebPartManager1.SelectedWebPartChanging 367 Chapter 11 If Me.WebPartManager1.SelectedWebPart Is Nothing Then ‘The first time that a Web Part has been . Visual Basic 20 05 code puts all of these together: Dim prs As System .Web. UI.WebControls.WebParts.WebPartPersonalization Dim prs As UI.WebControls.WebParts.WebPartPersonalization; prs = Me.WebPartManager1.Personalization If. As WebPart wp = Me.SearchZone.WebParts(“Search”) Me.WebPartManager1.CloseWebPart(wp) 3 70 Chapter 11 17_57860x ch11.qxd 10/ 4 /05 9: 32 PM Page 3 70 In C#: WebPart wp; wp = this.SearchZone.WebParts[“Search”]; this.WebPartManager1.CloseWebPart(wp); The. Sub 368 Chapter 11 17_57860x ch11.qxd 10/ 4 /05 9: 32 PM Page 368 In C#: bool bolChanging; protected void WebPartManager1_SelectedWebPartChanging(object sender, System .Web. UI.WebControls.WebParts.WebPartCancelEventArgs

Ngày đăng: 06/08/2014, 09:20

Từ khóa liên quan

Tài liệu cùng người dùng

  • Đang cập nhật ...

Tài liệu liên quan