Note that the information on this page is the BETA 1 of a guide that is now released. See http://www.codeplex.com/AppArchGuide for the latest PDF and HTML content.

Chapter 21 – Rich Internet Applications

- J.D. Meier, Alex Homer, David Hill, Jason Taylor, Prashant Bansode, Lonnie Wall, Rob Boucher Jr, Akshay Bogawat

Overview

Rich Internet Applications (RIAs) provide most of the deployment and maintainability benefits of Web applications, while supporting a much richer client UI. RIA implementations by different companies differ and should not be considered equal. For example, Microsoft Silverlight is designed to be a solid security-minded platform for professional LOB applications. In this document, the guidance is generalized with specifics contained in the “Technology Considerations” section.

ch21-fig1.JPG

Design Considerations

  • Consider when to use a RIA. RIA applications are well suited for Web-based scenarios where you need visualization beyond that provided by basic HTML. They are also perfect for streaming-media applications. They are less suited to extremely complex multi-page user interfaces. They are not suitable for scenarios requiring extensive access to local storage. Ease of deployment and maintenance, similar to that of a Web application, is also a reason to choose a RIA. RIAs are likely to have more consistent behavior and require less testing across the range of supported browsers when compared to Web applications that utilize advanced functions and code customizations.
  • Design for a Web infrastructure with services. RIA implementations require a similar infrastructure to Web applications. Communication to the business layer of your application is through services, which allows reuse of existing Web application infrastructure. Performance and responsiveness optimizations, which may transfer some logic to the client, should be considered later in the design process**
  • Design for running in the browser sandbox. RIA implementations have higher security by default, and so may not have access to all devices on a machine such as cameras and hardware video acceleration. Access to the local file system is limited. Local storage is available, but there is a maximum limit.
  • Determine your cross-platform requirements. RIAs support many different browser types on many operating systems. Consider whether you have control over the platform and browser where your application will run. It is preferable to design your application so that it can run on multiple platforms, although this may prevent you from using some platform-specific mechanisms.
  • Determine the complexity of your UI requirements. Consider the complexity of your user interface. RIA implementations work best when using a single screen for all operations. They can be extended to multiple screens, but this requires extra code and screen-flow consideration. Users should be able to easily navigate or pause, and return to the appropriate point in a workflow without restarting the whole process. For multi-page UIs, use deep linking methods. Also, manipulate the URL, the history list, and the browser's back and forward buttons to avoid confusion as users navigate between screens.
  • Use scenarios to increase application performance or responsiveness. List and examine the common application scenarios to decide how to intelligently divide and load modules, as well as how to cache or move business logic to the client. To reduce the download and start-up time for the application, intelligently segregate functionality into separate downloadable modules. Initially load only code stubs which can lazy-load other modules. Consider moving or caching regularly used business layer processes on the client for maximum application performance.
  • Design for scenarios where the plug-in is not installed. RIA implementations require a browser plug-in, and so you should design for non-interruptive plug-in installation. Consider if your clients have access to, have permission to, and will want to install the plug-in. Consider what control you have over the installation process. Plan for the scenario where users cannot install the plug-in by displaying an informative error message, or by providing an alternative Web user interface.

RIA Frame

Category Key Issues
Authentication and Authorization Failing to consider cross-domain issues.
Failing to consider cross-platform issues.
Business Layer Moving business operations to the client for reasons other than improved user experience or application performance.**
Failing to use profiling to identify expensive business operations that should be moved to the client
Trying to move all business processing to the client.
Failing to put business rules on the client into their own separate component to allow easy caching, updating and replacement.
Caching Failing to use isolated storage appropriately.
Failing to check and request increase of the isolated storage quota.
Failing to intelligently divide large client applications into smaller separately-downloadable components.
Downloading and instantiating the entire application at startup instead of intelligently and dynamically loading modules.
Communication Failing to consider and utilize the Silverlight asynchronous event handling model.
Using an incorrect binding strategy to the service interface.
Attempting to use sockets over unsupported or blocked ports.
Controls Adding custom control behavior through sub-classing instead of attaching new behavior to specific instances of controls.
Incorrect use of controls for a UI type.
Implementing custom controls when not required.
Composition Incorrectly implementing composition patterns, leading to dependencies that require frequent application redeployment.
Using composition when not appropriate for the scenario.
Not considering composition, and completely rewriting applications that could be reused with minimal or no changes.
Data Access Performing data access from the client.
Failing to filter data at the server.
Exception Management Failing to design an exception management strategy.
Failing to trap asynchronous call errors and unhandled exceptions. For example, not using the OnError event handler supported by Silverlight to trap exceptions in asynchronous calls.
Logging Failing to log critical errors.
Failing to consider a strategy to transfer logs to the server.
Failing to segregate logs by user and not by machine.
Media & Graphics Failing to take advantage of adaptive streaming for video delivery.
Assuming access to hardware acceleration on client.
Presentation Not pixel-snapping UI elements resulting in degraded UI presentation
Failing to trap browser navigation mechanisms causing user confusion
Not considering and designing for deep linking when necessary.
Portability Failing to consider the cost of testing for each platform and browser combination in a Web application compared to using a RIA interface.
Using platform-specific routines and support in code.
Re-writing code in development languages that are not portable and reusable.
State Management Failing to use isolated storage.
Using the server to store frequently-changing application state.
Failing to synchronize state between the client and server at the end of a session in a roaming scenario.
Validation Failing to identify trust boundaries and validate data moving across them
Failing to include extensive client side validation code in a separate downloadable module.

Authentication and Authorization

RIA clients can use the same authentication and authorization methods as Web clients. Authentication occurs at the Web layer.

When designing for authentication and authorization, consider the following guidelines:
  • Use Forms authentication with the ASP.NET authentication mechanism.
  • Consider that Windows authentication will not work for non-Windows users.
  • If you are authenticating through Web services, consider which bindings your RIA supports.

Business Layer

RIA implementations provide the capability to move business processing to the client. Consider moving logic that improves the user experience or performance of the application as a whole.

When designing the business layer, consider the following guidelines:
  • Consider starting with your business logic on the server exposed through Web services. Only move business logic to the client to improve the overall system performance or UI responsiveness to the user.
  • Use scenario-based profiling to discover and target routines that cause the heaviest server load or have the most impact on UI responsiveness. Consider moving or caching these routines on the client.
  • When putting business logic on the client, considering putting business rules or routines in a separate assembly that the application can load and update independently.
  • If you have to duplicate logic, attempt to the use same code language on the client and the server if your RIA implementation allows it.
  • If your RIA implementation allows creation of an instance without a UI, consider using it intelligently. You can keep your processing code in more structured, powerful, or familiar programming languages (such as C# or Python) instead of using less flexible browser-supported languages.
  • For security reasons, do not put highly sensitive unencrypted business logic on the client.

Caching

RIA implementations usually use the normal browser caching mechanism. However, Silverlight also provides an additional caching mechanism called isolated storage. Caching resources intelligently will improve application performance.

When designing a caching strategy, consider the following guidelines:
  • Cache components of your application for improved performance and fewer network round-trips. Allow the browser to cache objects that are not likely to change during a session. Utilize specific RIA local storage for information that changes during a session, or which should persist between sessions.
  • Use installation, updates, and user scenarios to derive intelligent ways to divide and load application modules.
  • Load stubs at start-up then dynamically load additional functionality in the background. Consider using events to intelligently pre-load modules just before they may be required.
  • To avoid unintended exceptions, check that local RIA storage is large enough to contain the data you will write to it. Storage space does not increase automatically; you must ask the user to increase it.

Communication

RIA implementations must use the asynchronous call model for services to avoid blocking browser processes. Cross-domain, protocol, and service-efficiency issues should be considered as part of your design.

When designing a communication strategy, consider the following guidelines:
  • If you have long-running code, consider using a background thread to avoid blocking the UI thread.
  • Call web-based services asynchronously to avoid blocking application processing.
  • Ensure that the RIA and the services it calls use compatible bindings that include security.
  • If your RIA client must access a server other than the one from which it was downloaded, ensure that you use a cross-domain configuration mechanism to permit access to the other servers/domains.
  • If client polling causes heavy server load, consider using sockets to proactively push data to the client.
  • Consider using sockets to push information to the server when this is significantly more efficient than using Web services; for example, real-time multi-player gaming scenarios utilizing a central server.

Controls

RIA implementations usually have their own native controls. You can often mix RIA-based and non-RIA based controls in the same UI, but extra communication code may be required.

When designing a strategy for controls, consider the following guidelines:
  • Use native RIA controls where possible.
  • If the appropriate control is not supplied with your RIA package, consider third-party RIA-specific controls.
  • If a native RIA control is not available, consider using a windowless RIA control in combination with an HTML or Windows Forms control that has the necessary functionality.
  • Avoid sub-classing controls to extend functionality if your RIA supports the ability to attach added behaviors to existing control implementations

Composition

Composition allows you to implement highly dynamic UIs that you can maintain without changes to the code and redeployment of the application. You can compose an application using RIA and non-RIA components.

When designing a composition strategy, consider the following guidelines:
  • Evaluate which composition model patterns best suit your scenario.
  • If an interface must gather information from many disparate sources, and those sources are user-configurable or change frequently, consider using composition.
  • When migrating an existing HTML application, consider mixing RIA and the existing HTML on the same page to reduce application reengineering.
  • Plan for the extra communication functionality by implementing it using JavaScript or services.

Data Access

RIA implementations access data in a similar way to normal Web applications. They should request data from the Web server through services in the same way as an AJAX client. After data reaches the client, it can be cached to maximize performance.

When designing a data access strategy, consider the following guidelines:
  • Do not attempt to use local client databases.
  • Minimize the number of round-trips to the server, while still providing a responsive user interface.
  • Filter data at the server rather than at the client to reduce the amount of data that must be sent over the network.
  • For operation-based services, utilize normal Web services.

Exception Management

A good exception management strategy is essential for correctly handling and recovering from errors in any application. In a RIA implementation, you must consider asynchronous exceptions as well as exception coordination between the client and server code.

When designing an exception management mechanism, consider the following guidelines:
  • Design an exception management strategy.
  • Use try/catch blocks to trap exceptions in synchronous code.
  • Put exception handling for asynchronous service calls in the separate handler designed for such exceptions.
  • Unhandled exceptions are passed to the browser, usually resulting in an unfriendly error message. Be sure to trap and include a friendly error message for unplanned exceptions.
  • Unhandled exceptions passed to the browser will allow continued execution once the user dismisses the error message. Trap unplanned exceptions. Stop program execution if continued execution would be harmful to the data integrity of application or mislead the user into thinking the application is still in a stable state.
  • Consider when to notify the server of client exceptions.
  • Do not use exception logic to control program flow.
  • Display user-friendly error messages.

Logging

Logging for the purpose of debugging or auditing can be challenging in a RIA implementation. Access to the client file system not possible in Silverlight applications, and execution of the client and the server proceed asynchronously. Log files from a client user must be combined with server log files to gain a full picture of program execution.

When designing a logging strategy, consider the following guidelines:
  • Consider the limitations of the logging component in the RIA implementation. For example, Silverlight can log to a user file, but not a general file for the whole client machine.
  • Determine a strategy to transfer client logs to the server for processing.
  • If using isolated storage for logging, consider the maximum size limit and the requirement to ask the user for an increase in storage capacity.
  • If using services to implement logging, consider the increased overhead.
  • Consider enabling logging and transferring logs to the server when exceptions are encountered.

Media and Graphics

RIA implementations provide a much richer experience and better performance than normal Web applications. Research and utilize the built-in media capabilities of your RIA platform. Keep in mind the features that may not be available on the RIA platform compared to a standalone player.

When designing for multimedia and graphics, consider the following guidelines:
  • Consider the cross-platform experience that is available from a single codebase t.
  • Design to utilize streaming media and video in the browser instead of invoking a separate player utility.
  • To increase performance, position media objects on whole pixels and present them in their native size
  • Use adaptive streaming to gracefully and seamlessly handle varying bandwidth issues.
  • Utilize the vector graphics engine for best drawing performance.
  • If programming an extremely graphics-intensive application, consider the lack of hardware acceleration.
  • Consider the lack of some advanced media player functions in the default RIA player, such as playback speed and graphic equalization.

Mobile

RIA implementations provide a much richer experience than a normal mobile application. Utilize the built-in media capabilities of the RIA platform you are using.

When designing for mobile device multimedia and graphics, consider the following guidelines:
  • Consider the cross-platform experience that is available from a single or similar codebase.
  • Design to utilize streaming media and video in the browser.
  • Use adaptive streaming to seamlessly handle varying bandwidth issues.
  • Utilize the vector graphics engine for better drawing performance.
  • If programming an extremely graphics intensive application, consider the lack of hardware acceleration and Graphics API support.

Portability

One of the main benefits of RIAs is the portability of code between different browsers, operating systems, and platforms. Using a single or similar RIA code base reduces development and maintenance time and cost while still providing platform flexibility.

When designing for portability, consider the following guidelines:
  • Design for the goal of "write once, run everywhere".
  • Consider the gains from using a RIA app over a Web application, such as the richer interface than can be provided. Differences between browsers can require extensive testing of code extensions. With a RIA application, the plug-in creator is responsible for consistency across different platforms.
  • Do not use features available only on one platform; for example, Windows Integrated Authentication. Design a solution based on standards that are portable across different clients.
  • RIA applications work on mobile devices, but consider using different XAML code on each type of device to reduce the impact of different screen sizes when designing for Windows Mobile,.
  • When possible, use development languages that are supported for both Rich Clients and RIAs. For example, Silverlight supports C#, Iron Python, Iron Ruby, and VB.NET. Most XAML code will also run in both WPF and Silverlight hosts.
  • Use library routines native to the RIA libraries.
  • Unlike Web applications, where you have to test on different platforms, a RIA application built on one platform should work on all other supported platforms. If not, this is likely to be a RIA compatibility issue, and these are extremely difficult to resolve.

Presentation

RIA applications work best when designed as one large central interface. Multi-page UIs require consideration on how you will link between pages. Positioning of elements on a page can affect both the look and performance of your RIA application.
When designing for presentation, consider the following guidelines:
  • To avoid anti-alias issues that cause fuzziness in RIAs that perform anti-aliasing, snap UI components to whole pixels. Pay attention to centering and math-based positioning routines. Consider writing a routine that checks for fractional pixels and rounds them to the nearest whole pixel value.
  • Trap the browser's forward and back button events to avoid unintentional navigation away from your page.
  • For multi-page UIs, use deep linking methods to allow unique identification and navigation to individual application pages.
  • For multi-page UIs, consider the ability to manipulate the browser's address text box content, history list, and back and forward buttons to implement normal Web page-like navigation.

State Management

You can store application state on the client using isolated storage if the state changes frequently. If application state is vital at startup, synchronize the client state to the server.

When designing for state management, consider the following guidelines:
  • Store state on the client in isolated storage to persist it during and between sessions.
  • Store the client state on the server only if loss of state on client would be catastrophic to the application’s function
  • Store the client state on the server if the client requires recovery of application state when using different accounts or when running on other hardware installations
  • If storing state on the server is necessary, synchronize the client and server at intelligent intervals or at the end of a session to avoid degraded performance.
  • Verify the stored state between the client and server at startup, and intelligently handle case when they are out of synchronization.

Validation

Validation must be performed using code on the client or through services located on the server. If you require more than trivial validation on the client, isolate validation logic in a separate downloadable assembly. This makes the rules easy to maintain.

When designing for validation, consider following guidelines:
  • Identify trust boundaries within the Web application layers, and validate data crossing these boundaries.
  • Consider performing validation on the server using services for rules that require access to server resources
  • For client-side validation, put validation code into routines that are separate from presentation or business logic.
  • If you have a large volume of client-side validation code, consider placing it into a separate downloadable module.
  • Use isolated storage to hold client-specific validation rules.
  • Assume all client-controlled data is malicious and revalidate it on the server.
  • Design your validation strategy using constrain, reject, and sanitize principles.
  • Design to validate input for length, range, format, and type.
  • Design to validate input from all sources such as the query string, cookies, and HTML controls.
  • Do not echo un-trusted input.
  • If you must write out un-trusted data, encode the output.
  • Use client-side validation to maximize user experience and server side validation for security.

Performance Considerations

Properly using the client-side processing power for a RIA application is one of the significant ways to maximize performance. Server-side optimizations similar to those used for Web applications are also a major factor.

Consider the following key performance guidelines:
  • Cache components of your application for improved performance and fewer network round-trips. Allow the browser to cache objects that are not likely to change during a session. Utilize specific RIA local storage for information that changes during a session, or should persist between sessions.
  • Use installation, updates, and user scenarios to derive intelligent ways to divide and load application modules.
  • Load stubs at start-up and then dynamically load additional functionality in the background. Consider using events to intelligently pre-load modules just before they may be needed.
  • Use scenario-based profiling to discover and target routines that cause the heaviest server load or have a major impact on UI responsiveness. Consider moving or caching these routines on the client.
  • When locating business logic on the client, place business rules or routines in a separate assembly that the application can load and update independently.
  • Design to utilize automatic bandwidth-varying streaming media mechanisms.
  • Utilize the vector graphics engine for best drawing performance.
  • To increase performance, position media objects on whole pixels and present them in their native size.

Security Considerations

RIA applications mitigate a variety of common attack vectors as they run inside a sandbox in the browser. Access to a number of resources is limited or restricted, which minimizes opportunities for attacks on the RIA and the client platform on which it runs.

Consider the following restrictions
  • Applications run inside a sandbox in the browser, within a memory space isolated from other applications.
  • Browsing of the local client file system is restricted.
  • Access to specialized local devices such as Webcams may be limited or not available.
  • Access to domains other than the one that delivered the application is limited, protecting the user from cross-site scripting attacks

Protect sensitive information using the follow methods:
  • The isolated storage mechanism provides a method of storing data locally, but does not provide built-in security. Do not store sensitive data locally unless it is encrypted using the platform encryption routines.
  • Create an exception management strategy to prevent exposure of sensitive information through unhandled exceptions.
  • Be careful when downloading sensitive business logic used on the client because tools that can extract the logic contained in downloaded XBAP files are available. Implement sensitive business logic through Web services. If the logic must be on the client for performance reasons, research and utilize any available obfuscation methods.
  • To minimize the amount of time that sensitive data is available on the client, utilize dynamic loading of resources and overwrite or clear components containing sensitive data from the browser cache.

Deployment Considerations

RIA implementations exhibit many of the benefits as Web applications in terms of deployment and maintainability. Design your RIA as modules that can be individually downloaded and cached to allow replacement of one module instead of the whole application. Version your application and components so you can detect the versions that clients are running.

When designing for deployment and maintainability, consider the following guidelines:
  • Consider how you will manage the scenario where the RIA browser plug-in is not installed.
  • Consider how you will redeploy modules when the application instance is still running on a client.
  • Divide the application into logical modules that can be cached separately, and can be replaced easily without requiring the user to download the entire application again.
  • Version your components.
  • Consider cross-platform issues concerning other supported browsers and operating systems.

Installation of the RIA plug-in

Intranet. If available, use application distribution software or the Group Policy feature of Active Directly to preinstall the plug-in on each computer in the organization. Alternatively, consider using Windows Update, where Silverlight is an optional component. Finally, consider manual installation through the browser, which requires the user to have Administrator privileges on the client machine.

Internet. Users must install the plug-in manually, so you should provide a link to the appropriate location to download the latest plug in. For Windows users, Windows Update provides the plug-in as an optional component.

Plug-in updates. In general, updates to the plug-in take into account backwards compatibility. However, consider implementing a plan to verify your application on new versions of the browser plug-in as they become available. For intranet scenarios, distribute a new plug-in after testing your application. In Internet scenarios, assume that automatic plug-in updates will occur. Test your application using the plug-in beta to ensure a smooth user transition when the plug-in is released.

Distributed Deployment

RIA implementations move presentation logic to the client, and so a distributed architecture is the most likely scenario for deployment.

In a distributed RIA deployment, the presentation logic is on the client and the business and data layers reside on the Web server or application server. Typically, you will have your business and data access layers on the same sever.

ch21-fig2.JPG

Consider the following guidelines:
  • If your applications are large, factor in the processing requirements for downloading the RIA components to clients.
  • If your business logic is shared by other applications, consider using distributed deployment.
  • A firewall between the client and the business layer is recommended for additional security.
  • If you use sockets in your application and you are not using port 80, consider which ports you must open in your firewall.
  • Design the presentation layer in such as way as it does not initiate, participate in, or vote on atomic transactions.
  • Consider using a message-based interface for your business logic.
  • Consider protecting sensitive data passed between different tiers.
  • If the business layer is accessed by other applications, consider using authentication in this layer.

Load Balancing

When you deploy your application on multiple servers, you can use load balancing to distribute RIA client requests to different servers. This improves response times, increases resource utilization, and maximizes throughput. Ensure that you use a crossdomain.xml file so that clients can access other domains where required.

ch21-fig3.JPG

Consider the following guidelines when designing your application to use load balancing:
  • Avoid server affinity. Server affinity occurs when all requests from a particular client must be handled by the same server. It is most often introduced by using locally updatable caches or in-process or local session state stores.
  • Consider storing all state on the client and designing stateless business components.
  • Consider using network load balancing software such as Windows Network Load Balancing (NLB) to implement redirection of requests to the servers in an application farm.

Web Farm Considerations

Consider using a Web farm that distributes requests from RIA clients to multiple servers. A Web farm allows you to scale out your application, and also reduces the impact of hardware failures. You can use either load balancing or clustering solutions to add more servers for your application.

Consider the following guidelines:
  • Consider using clustering to reduce the impact of hardware failures.
  • Consider partitioning your database across multiple database servers if your application has high I/O requirements.
  • Configure the Web farm to route all requests for the same user to the same server when you must support server affinity.
  • Do not use in-process session management in a Web farm unless you implement server affinity because requests from the same user cannot be guaranteed to be routed to the same server. Use the out-of-process session service or a database server for this scenario.

Pattern Map

Category Pattern
General Layered Architecture
Model View Controller
Plug-in
Business Layer Service Layer
Caching Page Cache
Communication Asynchronous Callback
Command
Controls Chain of Responsibility
Composition Composite View
Inversion of Control
Dispatcher View
Publish/Subscribe
Data Access Move Copy of Data
Page Layout View helper
Page Navigation Intercepting Filter
Work Flow

Key Patterns

  • Composite View. Combine individual views into a composite view.
  • Layered Application. Structure an application to support such operational requirements as maintainability, reusability, scalability, robustness, and security.
  • Model View Controller. Separate the user interface, the data repository features, and the code logic that binds the two together.
  • View Helper. Use Views to encapsulate formatting code and Helpers to encapsulate view-processing logic.

Technology Considerations

  • Silverlight 2.0 currently supports the Opera, Firefox, and Internet Explorer browsers though a plug-in. Silverlight 2.0 currently supports the Mac, Linux, Windows, and Windows Mobile operating systems for these browsers
  • The local storage mechanism for Silverlight 2.0 is called Isolated Storage. The current initial size is 1MB. The max storage size is 50MB. Silverlight requires that you ask the user to increase the storage size.
  • Silverlight 2.0 only supports Basic HTTP binding.
  • Windows Communication Foundation (WCF) in .NET 3.5 supports Basic HTTP binding, but security is not turned on by default. Be sure to turn on at least transport security to secure your service communications.
  • Silverlight 2.0 does not obfuscate modules downloaded as XBAP. XBAP can be decompiled and the programming logic extracted.
  • Silverlight 2.0 contains controls specifically designed for it. Third parties are likely to have additional control packages available.
  • Silverlight 2.0 does have a window-less control option that can be used with HTML, and Windows Forms controls.
  • Silverlight 2.0 allows you to attach added behaviors to existing control implementations. Use this approach instead of attempting to subclass a control.
  • Silverlight 2.0 supports only asynchronous calls to Web services.
  • Silverlight 2.0 calls use the OnError** event handler for an application when exceptions occur in services, or when synchronous exceptions are not handled.
  • Silverlight 2.0 does not currently support SOAP faults exposed by services due to the browser security model. Services must return exceptions to the client through a different mechanism.
  • Silverlight supports two file formats to deal with calling services cross-domain. You can use either a ClientAccessPolicy.xml file specific to Silverlight or a CrossDomain.xml file compatible with Adobe Flash. Place the file in the root of the server(s) to which your Silverlight client needs access.
  • In Silverlight 2.0, you must implement custom code for input and data validation.
  • Silverlight performs anti-aliasing for all UI components, so consider the recommendations in the Presentation area about snapping UI elements to whole pixels.
  • Consider using ADO.NET Data Services in a Silverlight application if large amounts of data need to be transferred from the server.
  • The cryptography APIs are available in Silverlight and should be utilized when storing and communicating sensitive data to the server if not already encrypted using another mechanism.

Additional Resources

For official information on Silverlight see the official Silverlight web site at http://silverlight.net/default.aspx

Last edited Dec 16, 2008 at 7:19 AM by rboucher, version 3

Comments

No comments yet.