Page Attributes in the Starter Site
In the last post I mentioned how attributes were used to affect page functionality. The main class behind that is PageContext and its army of attributes: PageGroupAttribute, PageLayoutAttribute, and SecureAttribute.
The attributes control various aspects of the page without requiring the page author to clutter the implementation with virtual method overrides or explicit initialization code. For instance, the PageGroupAttribute (described below) could just as easily been a virtual property on a base page class however that would have meant requiring all site pages to derive from a common class. Not unheard of by any means but limits the possibilities in the future.
Here is a list of the attributes
- PageGroupAttribute - The CS marketing system has the concept of a page group which can be used to target advertisements. For instance, if you want an advertisement to be displayed only on the home page you would set up a page group "Home" and set the advertisement to target the "Home" page group. See the Commerce Server Marketing documentation for more details.
- PageLayoutAttribute - This attribute controls how the page is arranged, specifically whether to display all ads and navigation or just a slimmed-down version (for checkout/user pages). Again, this could be done using something like multiple (perhaps layered) master pages but I felt it would constrain possibilities again. Using a class attribute allows us to hide all of that from the page itself so it doesn't need to know how the control hiding is done. You could throw the same page into a different set of master pages and it would just work.
- SecurePageAttribute - We knew going in that some pages contained sensitive data and we wanted to treat them differently. This attribute helps mitigate the Information Disclosure security threat by marking the cacheability of sensitive pages. If you look at the CheckPageSecurity method of the SiteModule class you will see the two actions that are taken. The first is to check that the page is being accessed over SSL. If it is not a secure connection (and the administrator hasn't disabled the requirement) then we redirect the user to the secure connection. This is a back up in case the administrator hasn't properly configured IIS. The second thing it does is set the cacheability of the page to Private; this keeps the page from showing up in any caches along the way except for the browser cache (we originally didn't even allow browser caching but that ended up with those "this page could not be displayed" problems when clicking the back button).
The two classes related to this are PageContext and PageSettings. The former provides an entry point into the page functions and the latter handles looking at the attributes and returning data. I won't go into PageContext too much because in my next post I'm going to refactor it away.
The key to PageSettings is the CreateSettingsForType method (everything else is just accessors and a cache). This method takes the type passed to it and retrieves it custom attributes. For each attribute we see if it belongs to a type we care about and set the appropriate field.
The other thing we do is store the result in a Dictionary to use as a cache. This is safe because a class can't change its attributes (at least not without some dark magic) without recompilation (which will trigger an app reset) so whatever we store in the cache can be held without it going out of date.
Since we store every class for the application lifetime (once the page has been fetched) this dictionary has the potential to grow very large if you have a site with thousands of individual pages. If you have this problem and want to decrease the working-set of your application you can move the cache operations to the ASP.NET cache or you can use an alternate implementation*.
Note the empty catch at the end of the method. We are doing a double-check lock (the first check happens before the call to CreateSettingsForType()). Theoretically an update can sneak in before the lock but not be picked up by the second check. In this situation the add will throw the exception. The only downside is that we did a little extra work but it's only a one-time thing. We can safely ignore the exception. Usually eating an exception like this isn't a good idea but occasionally it is safe. Just make sure you put an appropriate amount of thought into your decision.
In my next post I will walk you through a simple refactoring of these classes.
* One alternate implementation would be to use interfaces. One interface for "page group" and "page layout" and a marker interface for "secure page". Page settings could take an IHttpHandler, cast it to each of the interfaces, and an appropriately constructed PageSettings object without storing it in the cache.