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

mcts 70-562 Microsoft .NET Framework 3.5, ASP.NET Application Development phần 3 ppsx

108 346 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 108
Dung lượng 1,11 MB

Nội dung

Lesson 1: Using Client-Side State Management CHAPTER 4 187 nario each client provides all the information any Web server needs to process a request. With server-side state management, if a client switches servers in the middle of the session, the new server does not necessarily have access to the client’s state information (as it is stored on a different server). You can use multiple servers with server-side state management, but you need either intelligent load balancing (to always forward requests from a client to the same server) or centralized state manage- ment (where state is stored in a central database to which all Web servers have access). Storing information on the server has the following advantages: n Better security Client-side state management information can be captured (either in transit or while it is stored on the client) and maliciously modified. Therefore, you should never use client-side state management to store confidential information such as a password, authorization level, or authentication status. n Reduced bandwidth If you store large amounts of state management information, sending that information back and forth to the client can increase bandwidth utiliza- tion and page load times, potentially increasing your costs and reducing scalability. The increased bandwidth usage affects mobile clients most of all, because they often have very slow connections. Instead, you should store large amounts of state manage- ment data (say, more than 1 KB) on the server. The choice you make for managing application state should be decided based on these trade-offs. If you are writing an application with relatively few users and high security require- ments, you might consider leveraging server-side state. If you want to maximize for scalability but potentially slow down requests across slower bandwidth connections, you should rely on a heavy mix of client-side state. Of course, there is also persisted state, or data stored in the database. You need to factor this into your decision, too. You can decide to store all user information in the database and thus rely on it for state management. However, this often puts too much pressure on your database server. In this case it is best to store real, transactional data and rely on other tech- niques for managing more transient state. Finally, there is the concept of shared state. This is information common to many users of your application. In this case you can often use the caching features of ASP.NET to optimize for the heavy usage of this data. You might use application data caching to store com- monly accessed data from the database between user requests. You can also use page-level or fragment-level (partial page) caching to cache commonly accessed pages on the server. Again, the key is to get the right mix for your environment, application requirements, usage, and hardware. ASP.NET makes many tools and techniques available for you to manage state in your application. 1 8 8 CHAPTER 4 ASP.NET State Management View State As discussed in Chapter 2, “Adding and Conguring Server Controls,” view state is the default mechanism used by ASP.NET to store user-specific request and response data between page requests. The data being stored is typically specific to the controls on the page. View state stores object data that is not already represented as HTML in the page response. This ensures that data set on the server is preserved between round trips to the client and the server. Unless disabled, view state is part of every ASP.NET page. As an example, suppose a user requests a Web page that allows him or her to edit his or her own profile information. When processing the user’s request on the server, you might have to go out to a database and get the user’s profile information. You then use this information to set property values of the data entry fields on the page. When this page is sent to the user, these property value settings are wrapped up and stored in the view state. When the user then clicks a button to submit his or her changes back to the server, the user also sends back the view state as part of the Post- Back. ASP.NET uses this view state information to again set the property values of the server controls on the page back to what they were as part of the request. It then checks to see if any of these values were modified by the user as part of the PostBack request. Next, suppose there is an issue with processing the page on the server and therefore the server must return the same page back to the user. In this case, it again wraps the server control state (including any data changed by the user) back into the view state and sends it back to the client. You did not have to write this code; it just happens for you because of the ASP.NET view state client- side state management feature. The Page.ViewState property provides a dictionary object for retaining values between multiple requests for the same page. This object is of the type StateBag. When an ASP.NET page is processed, the current state of the page and its controls is hashed into a string and saved in the page as an HTML hidden field called __ViewState. If the data is too long for a single field (as specified in the Page.MaxPageStateFieldLength property), ASP.NET performs view state chunking to split it across multiple hidden fields. The following code sample dem- onstrates how view state adds data as a hidden form field within a Web page’s HTML: <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwULLTEzNjkxMzkwNjRkZAVvqsMGC6PVDmbCxBlPkLVKNahk" /> Notice that the view state values are hashed, compressed, and encoded for Unicode implementations. This provides better optimization and more security than just simple HTML hidden fields. The sections that follow describe how to work with ASP.NET view state. For most scenarios, it can be taken for granted. However, you might need to secure your view state data, disable view state data to increase performance, or add your own custom values to the view state. Lesson 1: Using Client-Side State Management CHAPTER 4 189 View State Security Considerations You need to be aware that view state can be tampered with, as it is simply a hidden field in the user’s browser. Of course, you should profile your application to better understand what risks you might face. An Internet application that works with private, personal information has a higher risk profile than that of an internal application that solves simple problems without using private (or secret) information. For most situations, you can rely on the fact that view state is hashed and encoded before being sent to the user’s browser. The view state also includes a message authentication code (MAC). This MAC is used by ASP.NET to determine if the view state has been tampered with. This helps ensure security in most situations without having to go to a fully encrypted view state. If you do have very sensitive information that is stored in the view state between page requests, you can encrypt it using the ViewStateEncryptionMode property of the Page object. This will secure the view state but will also decrease overall performance of the page process- ing due to the encrypting and decrypting of data. It will also increase the size of the data being sent between the browser and server. To enable view state encryption for your entire site, you set a value in your Web site configuration file. The viewStateEncryptionMode attribute of the pages element can be set to Always in the Web.config file. This tells ASP.NET to always encrypt your view state information for the entire site. An example of this setting in the configuration file is as follows: <configuration> <system.web> <pages viewStateEncryptionMode="Always"/> </system.web> </configuration> Alternatively, you can control view state encryption at the page level. This is useful for sce- narios in which sensitive information is confined to a single page or set of pages in your site. To do so, you again set the ViewStateEncryptionMode attribute to Always. However, you do so inside the individual page’s directive section. The following is an example: <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_ Default" ViewStateEncryptionMode="Always"%> Because view state supports encryption, it is considered the most secure(able) method of client-side state management. Encrypted view state is secure enough for most security requirements; however, it is more secure to store sensitive data on the server and not send it down to the client, where it has the potential to be manipulated and sent back to the server. Disabling View State Data View state is enabled by default for your page and all of the controls on the page. This includes controls like the Label control that you might never need to be part of view state. In addition, lots of view state data can cause performance problems. Remember, the view 1 9 0 CHAPTER 4 ASP.NET State Management state data is sent back and forth between browser and server. A larger view state means more information going across the wire and consequently longer waits for users. This includes processing time to package the view state, processing time to unpackage it, and bandwidth to transmit it to and from the client. You can minimize the data that gets stored and passed with the view state by setting the Control.EnableViewState property for each control on your page. Setting this property to false will instruct ASP.NET to not wire this control into the view state. This is useful if you do not need the control’s exact state between requests. Doing so will reduce server processing time and decrease page size. However, if you do need this state, you will either have to reenable view state for the control or code the repopulation of its data yourself. For most scenarios the view state size is not a big concern. It can be, however, for very large forms with a lot of data entry. It can also be overused when putting your own data into view state. If you think you have run into a bloated view state issue, you can use ASP.NET trace to examine the page and fi nd the culprit. Trace allows you to see the size of the view state for each page and each control on the page. For more information on working with ASP.NET tracing, see Lesson 2 in Chapter 12, “Troubleshooting a Running ASP.NET Applica- tion.” EXAM TIP Controls in ASP.NET have the ability to separate data state and control state. Previous ver- sions of ASP.NET stored data and control state together. When a control’s EnableViewState property was set to false, the control lost its appearance data along with the view state data. In the latest versions of ASP.NET (beyond 2.0), you can set a control’s EnableViewState to false and you will turn off the property value data but not the control’s appearance information. Of course, this also means that a control might still be contributing to the size of the view state even when the EnableViewState property is set to false. Reading and Writing Custom View State Data You can use view state to add and retrieve custom values that you need persisted between page requests. These values might not be part of a control but simply something you want to embed in the page to be returned as part of the next request. Adding a value to the view state collection is an effi cient and secure way to accomplish this task. The reading and writing of these collection values is as straightforward as working with the Dictionary collection. The following code demonstrates simple calls to write data into the view state and retrieve it from the collection. 'VB 'writing to view state Me.ViewState.Add("MyData", "some data value") 'read from view state Dim myData As String = CType(ViewState("MyData"), String) Lesson 1: Using Client-Side State Management CHAPTER 4 191 //C# //writing to view state this.ViewState.Add("MyData", "some data value"); //read from view state string myData = (string)ViewState["MyData"]; Adding data to the view state is great when you need the information passed back to the server as part of the page post. However, the content of the view state is for that page only. The view state does not transfer from one Web page to another. Therefore, it is useful only for temporarily storing values between requests to a single page. You can store a wide variety of object data inside the view state. You are not limited to just string values as you are with cookies. Instead, any data that can be serialized can be embed- ded in the view state. This includes classes in the .NET Framework that are marked serializable as well as classes you write and mark serializable. The following code shows an example of storing a DateTime object instance inside the ViewState without converting it to a string. 'VB 'check if ViewState object exists, and display it if it does If (Me.ViewState("lastVisit") IsNot Nothing) Then Dim lastVisit As DateTime = CType(Me.ViewState("lastVisit"), DateTime) Label1.Text = lastVisit.ToString() Else Label1.Text = "lastVisit ViewState not defined!" End If 'define the ViewState object for the next page view Me.ViewState("lastVisit") = DateTime.Now //C# //check if ViewState object exists, and display it if it does if (ViewState["lastVisit"] != null) Label1.Text = ((DateTime)ViewState["lastVisit"]).ToString(); else Label1.Text = "lastVisit ViewState not defined."; //define the ViewState object for the next page view ViewState["lastVisit"] = DateTime.Now; View State and Control State Recall that you can disable view state for a given control. This can be problematic for control developers. If you write custom controls (see Chapter 10, “Creating Custom Web Controls”), you might need view-state-like behavior that cannot be disabled by a developer. ASP.NET provides control state for just this purpose. 192 CHAPTER 4 ASP.NET State Management Control state allows you to store property value information that is specifi c to a control. Again, this state cannot be turned off and therefore should not be used in lieu of view state. To use control state in a custom Web control, your control must override the OnInit method. Here you call the Page.RegisterRequiresControlState method, passing an instance of your control to this method. From there, you override the SaveControlState method to write out your control state and the LoadControlState method to retrieve your control state. Quick Check 1. How do ASP.NET Web forms remember the settings for controls between user requests? 2. Is the view state lost if a user refreshes a Web page? What if the user e-mails a URL to a friend? Quick Check Answers 1. View state, which is enabled by default, is used to embed control property values to be sent to the client and back again to the server. 2. View state is embedded inside the HTML of a single instance of a Web page rest- ing in the user’s browser. It is lost and rewritten when a user refreshes his or her page. If the URL is copied and sent to another user, the view state does not go along. Instead, when new users request the page, they get their own view state instance. Hidden Fields As discussed, ASP.NET view state uses HTML hidden fi elds to store its data. Hidden fi elds in HTML are simply input fi elds that are embedded in a page’s HTML, not displayed to the user (unless the user chooses to view the page’s source), and then sent back to the server on the page post. ASP.NET provides a control for creating your own custom hidden fi elds in a similar man- ner as you would create and use other ASP.NET controls. The HiddenField control allows you to store data in its Value property. You add HiddenField controls to your page the way you would any other control (drag from the Toolbox). Like view state, hidden fi elds only store information for a single page. Therefore, they are not useful for storing session data that is used between page requests. Unlike view state, hidden fi elds have no built-in compression, encryption, hashing, or chunking. Therefore users can view or modify data stored in hidden fi elds. To use hidden fi elds, you must submit your pages to the server using HTTP POST (which happens in response to a user pressing a submit button). You cannot simply call an HTTP GET (which happens if the user clicks a link) and retrieve the data in the hidden fi eld on the server. Quick Check 1 . How do ASP.NET Web forms remember the settings for controls between user requests? 2 . Is the view state lost if a user refreshes a Web page? What if the user e-mails a URL to a friend? Quick Check Answers 1 . View state, which is enabled by default, is used to embed control property values to be sent to the client and back again to the server. 2 . View state is embedded inside the HTML of a single instance of a Web page rest- ing in the user’s browser. It is lost and rewritten when a user refreshes his or her page. If the URL is copied and sent to another user, the view state does not go along. Instead, when new users request the page, they get their own view state instance. 1 2 1 2 Lesson 1: Using Client-Side State Management CHAPTER 4 193 Cookies Web applications often need to track users between page requests. These applications need to ensure that the user making the fi rst request is the same user making subsequent requests. This type of common tracking is done with what are called cookies. A cookie is a small amount of data that you write to the client to be stored and then passed with requests to your site. You write persistent cookies to a text fi le on the client machine. These cookies are meant to survive the user shutting down the browser and reopening it at a later time. You can also write temporary cookies to the memory of the client’s browser. These cookies are used only during the given Web session. They are lost when the browser closes. Again, the most common use of cookies is to identify a single user as he or she visits mul- tiple Web pages within your site. However, you can also use cookies to store state information or other user preferences. Figure 4-2 illustrates how a Web client and a server use cookies. First (Step 1), the Web client requests a page from the server. Because the client has not visited the server before, it does not have a cookie to submit. When the Web server responds to the request (Step 2), the Web server includes a cookie in the response; this cookie is written to the user’s browser or fi le system. The Web client then submits that cookie with each subsequent request for any page on the same site (Steps 3, 4, and any future page views). Client Initial page request 1 Respond with cookie 2 Pass cookie 3 4 Pass cookie Web Server FIGURE 4-2 Web servers use cookies to track Web clients NOTE ASP.NET SESSIONS AND COOKIES By default, ASP.NET uses cookies to track user sessions. If you have enabled session state, ASP.NET writes a cookie to the user’s browser and uses this cookie to identify his or her server session. Cookies are the most fl exible and reliable way of storing data on the client. However, users can delete cookies on their computers at any time. You can set cookies to have long expira- tion times but that does not stop users from deleting all their cookies and thus wiping out any settings you might have stored in them. In addition, cookies do not solve the issue of a user moving from computer to computer. In these cases, users’ preferences do not always go along with them. Therefore, if you allow a lot of personalization for users of your site, you NOTE ASP.NET SESSIONS AND COOKIES By default, ASP.NET uses cookies to track user sessions. If you have enabled session state, ASP.NET writes a cookie to the user’s browser and uses this cookie to identify his or her server session. 1 9 4 CHAPTER 4 ASP.NET State Management need to allow them to log in and reset their cookies. Doing so should then reenable their customizations provided you have them stored elsewhere. Reading and Writing Cookies A Web application creates a cookie by sending it to the client as a header in an HTTP re- sponse. Of course, ASP.NET makes writing to and reading from the cookie collection a rela- tively straightforward task. To add a cookie to the cookies collection and have it written out to the browser, you call the Response.Cookies.Add method. The Cookies property of the Page.Response property is of the type HttpCookieCollection. You add instances of HttpCookie to this collection. The HttpCookie object simply contains a Name property and a Value property. The following code shows how you might add an item to the cookies collection: Response.Cookies.Add(New HttpCookie("userId", userId)) To retrieve a cookie sent back by the Web browser, you read the values in the Request. Cookies collection. The following shows an example of this code: Request.Cookies("userId").Value As a larger example, the following sample code in a Page_Load event handler demon- strates both defining and reading cookie values by setting a cookie named lastVisit to the current time. If the user already has the cookie set, the code displays in the Label1 control the time the user last visited the page. 'VB 'check if cookie exists, and display it if it does If Not (Request.Cookies("lastVisit") Is Nothing) Then 'encode the cookie in case the cookie contains client-side script Label1.Text = Server.HtmlEncode(Request.Cookies("lastVisit").Value) Else Label1.Text = "No value defined" End If 'define the cookie for the next visit Response.Cookies("lastVisit").Value = DateTime.Now.ToString Response.Cookies("lastVisit").Expires = DateTime.Now.AddDays(1) //C# //check if cookie exists, and display it if it does if (Request.Cookies["lastVisit"] != null) //encode the cookie in case the cookie contains client-side script Label1.Text = Server.HtmlEncode(Request.Cookies["lastVisit"].Value); else Label1.Text = "No value defined"; //define the cookie for the next visit Lesson 1: Using Client-Side State Management CHAPTER 4 195 Response.Cookies["lastVisit"].Value = DateTime.Now.ToString(); Response.Cookies["lastVisit"].Expires = DateTime.Now.AddDays(1); The fi rst time the user visits the page in the previous example, the code displays “No value defi ned” because the cookie has not yet been set. However, if you refresh the page, it displays the time of the fi rst visit. Note that the code sample defi nes the Expires property for the cookie. You must defi ne the Expires property and set it for the time period you would like the client to store the cookie if you want the cookie to persist between browser sessions. If you do not defi ne the Expires property, the browser stores the cookie in memory and the cookie is lost if the user closes his or her browser. To delete a cookie, you overwrite the cookie and set an expiration date in the past. You cannot directly delete cookies because they are stored on the client’s computer. NOTE VIEWING AND TROUBLESHOOTING COOKIES You can use Trace.axd to view cookies for every page request. For more information, see Chapter 12, “Monitoring, Troubleshooting, and Debugging.” Controlling Cookie Scope Cookies should be specifi c to a given Web site’s domain or a directory within that domain. The information in cookies is typically specifi c to that site and often private. For this reason a browser should not send your cookie to another site. By default, browsers will not send your cookie to a Web site with a different host name (although, in the past, vulnerabilities in browsers have allowed attackers to trick a browser into submitting another Web site’s cookie). You have control over a cookie’s scope. You can limit the scope to either a specifi c direc- tory on your Web server or expand the scope to the entire domain. The scope of your cookie determines which pages have access to the information embedded in the cookie. If you limit the scope to a directory, only pages in that directory will have access to the cookie. You control cookie scope on a per-cookie basis. To limit the scope of a cookie to a directory, you set the Path property of the HttpCookie class. The following shows sample code for doing just that: 'VB Response.Cookies("lastVisit").Value = DateTime.Now.ToString Response.Cookies("lastVisit").Expires = DateTime.Now.AddDays(1) Response.Cookies("lastVisit").Path = "/MyApplication" //C# Response.Cookies["lastVisit"].Value = DateTime.Now.ToString(); Response.Cookies["lastVisit"].Expires = DateTime.Now.AddDays(1); Response.Cookies["lastVisit"].Path = "/MyApplication"; NOTE VIEWING AND TROUBLESHOOTING COOKIES You can use Trace.axd to view cookies for every page request. For more information, see Chapter 12, “Monitoring, Troubleshooting, and Debugging.” 1 9 6 CHAPTER 4 ASP.NET State Management With the scope limited to “/MyApplication”, the browser submits the cookie to any page in the /MyApplication folder. However, pages outside of this folder do not get the cookie, even if they are on the same server. To expand the scope of a cookie to an entire domain, set the Domain property of the HttpCookie class. The following code demonstrates: 'VB Response.Cookies("lastVisit").Value = DateTime.Now.ToString Response.Cookies("lastVisit").Expires = DateTime.Now.AddDays(1) Response.Cookies("lastVisit").Domain = "contoso.com" //C# Response.Cookies["lastVisit"].Value = DateTime.Now.ToString(); Response.Cookies["lastVisit"].Expires = DateTime.Now.AddDays(1); Response.Cookies["lastVisit"].Domain = "contoso.com"; Setting the Domain property to “contoso.com” causes the browser to submit the cookie to any page in the contoso.com domain. This might include those pages that belong to the sites www.contoso.com, intranet.contoso.com, or private.contoso.com. Similarly, you can use the Domain property to specify a full host name, limiting the cookie to a specific server. Storing Multiple Values in a Cookie The size of your cookie is dependent on the browser. Each cookie can be up to a maximum of 4 KB in length. In addition, you can typically store up to 20 cookies per site. This should be more than sufficient for most sites. However, if you need to work around the 20-cookie limit, you can store multiple values in a single cookie by setting the given cookie’s name and its key value. The key value is usually not used when storing just a single value. However, if you need multiple values in a single named cookie, you can add multiple keys. The following code shows an example: 'VB Response.Cookies("info")("visit") = DateTime.Now.ToString() Response.Cookies("info")("firstName") = "Tony" Response.Cookies("info")("border") = "blue" Response.Cookies("info").Expires = DateTime.Now.AddDays(1) //C# Response.Cookies["info"]["visit"].Value = DateTime.Now.ToString(); Response.Cookies["info"]["firstName"].Value = "Tony"; Response.Cookies["info"]["border"].Value = "blue"; Response.Cookies["info"].Expires = DateTime.Now.AddDays(1); Running this code sends a single cookie to the Web browser. However, that cookie is parsed to form three values. ASP.NET then reads these three values back in when the cookie is submitted back to the server. The following shows the value sent to the Web browser: [...]... life cycle of an ASP.NET application when working with server-side state management This life cycle defines how the application server starts and stops your application, isolates it from other applications, and executes your code Your ASP.NET application runs based on the server application that hosts it This typically means IIS There are multiple versions of IIS that can run your ASP.NET application, ... of your application, such as Microsoft Internet Information Services (IIS), might restart your ASP.NET application In addition, the application is also restarted if the server is restarted To work with this constraint, you should understand how to read, write, and sometimes persist application state using the application events described later in this lesson The ASP.NET Application Life Cycle It is... use of profile properties in ASP.NET Estimated lesson time: 30 minutes Application State Application state in ASP.NET is a global storage mechanism for state data that needs to be accessible to all pages in a given Web application You can use application state to store information that must be maintained between server round trips and between requests for pages Again, application state is optional;... scoped at the application level This event, along with Application_ End, is a special event in ASP They do not map back to the HttpApplication object ASP.NET State Management n Application_ End  The Application_ End event is raised when your application stops or shuts down This event is useful if you need to free application- level resources or perform some sort of logging This event, along with Application_ Start,... service called the ASP.NET State Service This ensures that session state is preserved if the Web application is restarted and also makes session state available to multiple Web servers in a Web farm ASP.NET State Service is included with any computer set up to run ASP.NET Web applications; however, the service is set up to start manually by default Therefore, when configuring the ASP.NET State Service,... imultaneously s 2 12 CHAPTER 4 ASP.NET State Management on multiple threads, you must lock the Application object when making calculations and performing updates to application- level data For example, the following code locks the Application object for a single thread before incrementing and updating an application- level variable: 'VB Application. Lock() Application( "PageRequestCount") = CInt (Application( "PageRequestCount"))... - 1 Application. UnLock() End Sub //C# void Application_ Start(object sender, EventArgs e) { Application[ "UsersOnline"] = 0; } void Session_Start(object sender, EventArgs e) { Application. Lock(); Application[ "UsersOnline"] = (int )Application[ "UsersOnline"] + 1; Application. UnLock(); } void Session_End(object sender, EventArgs e) { Application. Lock();... CInt (Application( "clicks")) + 1 Application. UnLock() LabelApplicationClicks.Text = "Application clicks: " + _ Application( "clicks").ToString End Sub //C# protected void Page_Load(object sender, EventArgs e) { Application. Lock(); Application[ "clicks"] = ((int )Application[ "clicks"]) + 1; Application. UnLock(); LabelApplicationClicks.Text = "Application clicks: " + Application[ "clicks"].ToString(); } 8 Build your Web site and... versions This section outlines how your pages are processed by these servers to give you a basic understanding of how you can affect application state management The following stages constitute the application life cycle of an ASP.NET application: 1 The life cycle of an ASP.NET application begins when a user first makes a request for a page in your site 2 The request is routed to the processing pipeline... information."; } 2 14 CHAPTER 4 ASP.NET State Management //define the Session object for the next page view Session["lastVisit"] = DateTime.Now; NOTE sessiOn state and cOOkies ASP.NET writes a cookie to the client’s machines to track their session This cookie is called aSP.neT_ SessionId and contains a random 24-byte value Requests submit this cookie from the browser and ASP.NET maps the cookie’s value . environment, application requirements, usage, and hardware. ASP .NET makes many tools and techniques available for you to manage state in your application. 1 8 8 CHAPTER 4 ASP .NET State Management View. more information on working with ASP .NET tracing, see Lesson 2 in Chapter 12, “Troubleshooting a Running ASP .NET Applica- tion.” EXAM TIP Controls in ASP .NET have the ability to separate data. for users of your site, you NOTE ASP .NET SESSIONS AND COOKIES By default, ASP .NET uses cookies to track user sessions. If you have enabled session state, ASP .NET writes a cookie to the user’s

Ngày đăng: 12/08/2014, 20:22

TỪ KHÓA LIÊN QUAN

TÀI LIỆU CÙNG NGƯỜI DÙNG

TÀI LIỆU LIÊN QUAN