The Spring.NET Web Framework increases your productivity when you write ASP.NET WebForms applications by offering capabilities not found in other .NET web frameworks.
The Spring.NET Web Framework makes it easy to write 'thin and clean' web applications. "Thin" refers to WebForm's role as a small as possible adapter between the HTML- based world of the web and the Object-oriented world of your application. The business logic does not reside in the web tier; it resides in the application layer with which your web form communicates. "Clean" refers to the framework's appropriate separation of concerns, separating web specific processing such as copying data out and into from element from a data model from calling into a buiness tier and redirecting to the next page. This results in an event-handler that does not contain any reference to UI elements thereby making it possible to test your event handler code in integration style tests. The Spring.NET Web Framework reduces the incidental complexity of common tasks in the web tier, for example, the conversion of HTML control data to objects and then vice-versa after the request is processed by the application layer.
Highlights of Spring's Web framework are:
Dependency Injection. Provided for all ASP.NET artifacts, inlcuding pages and user controls, modules, providers, and HTTP handlers. Your pages, controls, and so on do not require any dependency on Spring in order to be configured via dependency injection.
Bidirectional data binding. Allows you to declaratively define the data that will be marshaled out of your HTML and user controls and into a data model that in turn is generally submitted to the application layer. After the data model is updated in the application layer, those changes are automatically reflected in the HTML and user controls on post back. This process removes large amounts of tedious, error-prone boilerplate code.
Web object scopes. Can be defined at the application, session, or request scope. This capability makes it easy to inject, for example, a session scoped shopping cart, into your page without any lower level programming.
Data model management. Provides a mechanism similar to view state to help manage your data model. (While ASP.NET manages the view state of your form, it does not offer facilities to manage the data model that you build up to submit to the application layer.)
UI-agnostic validation framework. Enables you to declaratively define complex validation rules, for example, that take into account complex relationships in your data model. Spring's error controls easily render validation failure. Thus you can centralize your validation logic and also reuse it on the server side, for example, by using parameter validation advice described in the aspect library chapter.
Externalized page navigation through result mapping. Instead of hard-coding URLs and data to direct where a page should go next and what data should be carried along, you can define and configure result mappings externally that associate logical names and a URL (+ data). This capability also allows you to encrypt the values that are sent through Response.Redirect.
Improved localization and master page support. Provides advanced localization features (including image localization) and make it easy to declaratively configure which master page to apply to different parts of your web application.
All you know about ASP.NET development still applies. Spring's approach is to 'embrace and extend' the basic ASP.NET programming model to make you as productive as possible.
Note | |
---|---|
Support for ASP.NET MVC is planned for Spring.NET 2.0. |
This chapter describes the Spring.NET Web Framework in detail. The framework is not an all-or-nothing solution. For example, you can choose to use only dependency injection and bi-directional data binding. You can adopt the web framework incrementally, addressing problems areas in your current web application with a specific feature.
The Spring.NET distribution ships with a Web Quick Start application and a complete reference application, SpringAir. The Web QuickStart is the best way to learn each Spring.NET Web Framework (also referred to in this document as Spring.Web) feature, by following simple examples. The SpringAir reference application has a Spring.Web-enabled frontend that uses many best practices for Spring.NET web applications, so refer to it as you are reading this (reference) material (see Chapter 41, SpringAir - Reference Application).
Many developers dislike the ASP.NET programming model because currently it is not a "true MVC" (model-view-controller) implementation; controller-type logic within the page is too tightly coupled to the view. For example, event handlers within the page class typically have references to view elements, such as input controls, in many code behind locations, most typically the event handler. Controller-type logic, such as the code within page event handlers in ASP.NET, should not depend on the view elements.
However, ASP.NET has its good points. Server-side forms and controls make developers significantly more productive and allow you to significantly simplify page markup. They also make cross-browser issues easier to deal with, as each control can make sure that it renders correct markup based on the user's browser. The ability to hook custom logic into the lifecycle of the page, as well as to customize the HTTP processing pipeline, are also very powerful features. The ability to interact with the strongly typed server-side controls instead of manipulating string-based HTTP request collections, such as Form and QueryString, is a much needed layer of abstraction in web development.
Thus, instead of developing a new, pure and true MVC web framework as part of Spring.NET, Spring decided to extend ASP.NET so that most of its shortcomings are eliminated. With the introduction of a 'true MVC framework' to .NET there are several opportunities for integration with IoC containers such as Spring.NET. Furthermore, as Spring for Java has a very popular MVC framework, much of that experience and added value can be transliterated to help developers be more productive when using Spring's future support for ASP.NET MVC.
Spring.Web also supports the application of the dependency injection
principle to one's ASP.NET Pages
and
Controls
as well as to HTTP modules and custom provider
modules. Thus application developers can easily inject service
dependencies into web controllers by leveraging the power of the
Spring.NET IoC container. See Dependency Injection
for ASP.NET Pages.
Event handlers in code-behind classes should not have to deal with
ASP.NET UI controls directly. Such event handlers should rather work with
the presentation model of the page, represented either as a hierarchy of
domain objects or an ADO.NET DataSet
. Spring.NET
implemented a bidirectional data binding framework to handle the mapping
of values to and from the controls on a page to the underlying data model.
The data binding framework also transparently implements data type
conversion and formatting, enabling application developers to work with
fully typed data (domain) objects in the event handlers of code-behind
files. See Bidirectional Data Binding and
Model Management.
The Spring.NET Web Framework also addresses concerns about the flow
of control through an application. Typical ASP.NET applications use
Response.Redirect
or Server.Transfer
calls within Page
logic to navigate to an appropriate
page after an action is executed. This usage often leads to hard-coded
target URLs in the Page
, which is never a good thing.
Result mapping solves this problem by allowing application developers to
specify aliases for action results that map to target URLs based on
information in an external configuration file that can easily be edited.
See Result Mapping.
Standard localization support is also limited in versions of ASP.NET
prior to ASP.NET 2.0. Even though Visual Studio 2003 generates a local
resource file for each ASP.NET Page
and user control,
those resources are never used by the ASP.NET infrastructure. This means
that application developers have to deal directly with resource managers
whenever they need access to localized resources, which in the opinion of
the Spring.NET team should not be the case. Spring.Web adds comprehensive
support for localization using both local resource files and global
resources that are configured within and for a Spring.NET container. See
Localization and Message
Sources.
In addition to the aforementioned core features, Spring.Web ships with lesser features that might be useful to many application developers. Some of these additional features include back-ports of ASP.NET 2.0 features that can be used with ASP.NET 1.1, such as Master Page support. See Master Pages in ASP.NET 1.1 .
To implement some features, the Spring.NET team had to extend (as in
the object-oriented sense) the standard ASP.NET Page
and UserControl
classes. This means that in order to
take advantage of the full feature stack of
Spring.Web (most notably bidirectional data binding, localization and
result mapping), your code-behind classes must extend Spring.Web specific
base classes such as Spring.Web.UI.Page
. However,
powerful features such as dependency injection for ASP.NET Pages,
controls, and providers can be leveraged without having to extend
Spring.Web-specific base classes. By taking advantage of
some of the more useful features offered by
Spring.Web, you will be coupling the presentation tier of your
application(s) to Spring.Web. The choice of whether or not this is
appropriate is, of course, left to you.
Spring.Web builds on top of the Spring.NET IoC container, and
makes heavy use (internally) of the easy pluggability and standardized
configuration afforded by the IoC container. ASP.NET
Page
s and UserControls that make up a typical
Spring.Web-enabled application are configured with the same standard
Spring.NET XML configuration syntax used for non web objects. To
integrate with the ASP.NET runtime you need to make a few modifications
to your Web.config file.
Spring.Web uses a custom PageHandlerFactory
implementation to load and configure a Spring.NET IoC container, which
is in turn used to locate an appropriate Page
to
handle a HTTP request. The WebSupportModule
configures miscellaneous Spring infrastructure classes for use in a web
environment, for example setting the storage strategy of
LogicalThreadContext
to be
HybridContextStorage
.
The instantiation and configuration of the Spring.NET IoC
container by the Spring.Web infrastructure is wholly transparent to
application developers, who typically never have to explicitly
instantiate and configure an IoC container manually (by, for example,
using the new
operator in C#). To effect the
transparent bootstrapping of the IoC container, you need to insert the
following configuration snippet into the root
Web.config
file of every Spring.Web-enabled web
application. (You can of course change the verb
and
path
properties from the values that are shown.)
Note | |
---|---|
If you are using the solution templates that ship with Spring.NET this configuration will be done for you automatically whent he solution is created. |
<system.web> <httpHandlers> <add verb="*" path="*.aspx" type="Spring.Web.Support.PageHandlerFactory, Spring.Web"/> </httpHandlers> <httpModules> <add name="Spring" type="Spring.Context.Support.WebSupportModule, Spring.Web"/> </httpModules> ... </system.web>
This snippet of standard ASP.NET configuration is only required in
the root directory of each Spring.Web web
application (that is, in the Web.config
file present
in the top level virtual directory of an ASP.NET web
application).
The above XML configuration snippet directs the ASP.NET
infrastructure to use Spring.NET's page factory, which in turn creates
instances of the appropriate .aspx
Page
, possibly injects dependencies into that
Page
(as required), and then forwards the handling of
the request to the Page
.
After the Spring.Web page factory is configured, you also need to
define a root application context by adding a Spring.NET configuration
section to that same Web.config
file. The final
configuration file should resemble the following; your exact
configuration may vary in particulars.
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <sectionGroup name="spring"> <section name="context" type="Spring.Context.Support.WebContextHandler, Spring.Web"/> </sectionGroup> </configSections> <spring> <context> <resource uri="~/Config/CommonObjects.xml"/> <resource uri="~/Config/CommonPages.xml"/> <!-- TEST CONFIGURATION --> <!-- <resource uri="~/Config/Test/Services.xml"/> <resource uri="~/Config/Test/Dao.xml"/> --> <!-- PRODUCTION CONFIGURATION --> <resource uri="~/Config/Production/Services.xml"/> <resource uri="~/Config/Production/Dao.xml"/> </context> </spring> <system.web> <httpHandlers> <add verb="*" path="*.aspx" type="Spring.Web.Support.PageHandlerFactory, Spring.Web"/> </httpHandlers> <httpModules> <add name="Spring" type="Spring.Context.Support.WebSupportModule, Spring.Web"/> </httpModules> </system.web> </configuration>
Notes about the preceding configuration:
Define a custom configuration section handler for the
<context
>
element. If you use
Spring.NET for many applications on the same web server, it might be
easier to move the whole definition of the Spring.NET section group
to your machine.config
file.
The custom configuration section handler is of the type
Spring.Context.Support.WebContextHandler
which in
turn instantiates an IoC container of the type
Spring.Context.Support.WebApplicationContext
.
This ensures that all features provided by Spring.Web, such as
request and session-scoped object definitions, are handled
properly.
Within the <spring>
element, define a root
context element. Next, specify resource locations that contain the
object definitions that are used within the web application (such as
service or business tier objects) as child elements within the
<context> element. Object definition
resources can be fully-qualified paths or URLs, or non-qualified, as
in the example above. Non-qualified resources are loaded using the
default resource type for the context, which for the
WebApplicationContext
is the
WebResource
type.
The object definition resources do not have to be the same
resource type (for example, all file://
, all
http://
, all assembly://
, and
so on). This means that you can load some object definitions from
resources embedded directly within application assemblies
(assembly://
) while continuing to load other
object definitions from web resources that can be more easily
edited.
There is some configuration that is specific to using IIS7, the appropriate code snippit to place in web.config shown below.
<system.webServer> <validation validateIntegratedModeConfiguration="false"/> <modules> <add name="Spring" type="Spring.Context.Support.WebSupportModule, Spring.Web"/> </modules> <handlers> <add name="SpringPageHandler" verb="*" path="*.aspx" type="Spring.Web.Support.PageHandlerFactory, Spring.Web"/> <add name="SpringContextMonitor" verb="*" path="ContextMonitor.ashx" type="Spring.Web.Support.ContextMonitor, Spring.Web"/> </handlers> </system.webServer>
ASP.NET has a hierarchical configuration mechanism that enables application developers to override configuration settings specified at a higher level in the web application directory hierarchy with configuration settings specified at the lower level.
For example, a web application's root
Web.config
file overrides settings from the (higher
level) machine.config
file. In the same fashion,
settings specified within the web.config
file within
a subdirectory of a web application will override settings from the root
Web.config
and so on. You can also add seettings to
lower level Web.config
files that were not previously
defined anywhere.
Spring.Web leverages this ASP.NET feature to provide support for a
context hierarchy. You can add new object definitions to lower level
Web.config
files or override existing ones per
virtual directory.
What this means to application developers is that one can easily componentize an application by creating a virtual directory per component and creating a custom context for each component that contains the necessary configuration info for that particular context. The configuration for a lower level component generally contains only those definitions for the pages that the component consists of and (possibly) overrides for some definitions from the root context (for example, menus).
Because each such lower level component usually contains only a
few object definitions, application developers are encouraged to embed
those object definitions directly into the Web.config
for the lower level context instead of relying on an external resource
containing object definitions. This is easily accomplished by creating a
component Web.config
similar to the following
one:
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <sectionGroup name="spring"> <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core"/> </sectionGroup> </configSections> <spring> <context type="Spring.Context.Support.WebApplicationContext, Spring.Web"> <resource uri="config://spring/objects"/> </context> <objects xmlns="http://www.springframework.net"> <object type="MyPage.aspx" parent="basePage"> <property name="MyRootService" ref="myServiceDefinedInRootContext"/> <property name="MyLocalService" ref="myServiceDefinedLocally"/> <property name="Results"> <!-- ... --> </property> </object> <object id="myServiceDefinedLocally" type="MyCompany.MyProject.Services.MyServiceImpl, MyAssembly"/> </objects> </spring> </configuration>
The <context/>
element seen above
(contained within the <spring/>
element) simply
tells the Spring.NET infrastructure code to load (its) object
definitions from the spring/objects
section of the
web.config configuration file.
If Spring.NET is used for multiple applications on the same
server, you can avoid the need to specify the
<configSections/>
element as shown in the
previous example, by moving the configuration handler definition for the
<objects>
element to a higher level (root)
Web.config
file, or even to the level of the
machine.config
file.
This component-level context can reference definitions from its parent context(s). If a referenced object definition is not found in the current context, Spring.NET searches all ancestor contexts in the context hierarchy until it finds the object definition (or ultimately fails and throws an exception).
An example of how Spring.Web builds on the capabilities of ASP.NET
is the way in which Spring.Web has used the code-behind class of the
Page
mechanism to satisfy the
Controller
portion of the MVC architectural pattern. In
MVC-based (web) applications, the Controller
is
typically a thin wrapper around one or more service objects. It is
important that service object dependencies be easily injected into
Page
Controller
s. Accordingly,
Spring.Web provides first class support for dependency injection in
ASP.NET Page
s. Application developers can inject any
required service object dependencies (and indeed any other dependencies)
into their Page
s using the standard Spring.NET
configuration instead of having to rely on custom service locators or
manual object lookups in a Spring.NET application context.
After an application developer configures the Spring.NET web application context, the developer can easily create object definitions for the pages that compose that web application.
<objects xmlns="http://www.springframework.net"> <object name="basePage" abstract="true"> <property name="MasterPageFile" value="~/Web/StandardTemplate.master"/> </object> <object type="Login.aspx"> <property name="Authenticator" ref="authenticationService"/> </object> <object type="Default.aspx" parent="basePage"/> </objects>
The preceding example contains three definitions:
An abstract definition for the base page from which many other pages in the application will inherit. In this case, the definition simply specifies which page is to be referenced as the master page, but it typically also configures localization-related dependencies and root folders for images, scripts, and CSS stylesheets.
A login page that neither inherits from the base page nor
references the master page. This page shows how to inject a service
object dependency into a page instance (the
authenticationService
is defined elsewhere).
A default application page that, in this case, simply inherits from the base page in order to inherit the master page dependency, but apart from that it does not need any additional dependency injection configuration.
The configuration of ASP.NET pages differs from the configuration of
other .NET classes in the value passed to the type
attribute. As can be seen in the above configuration snippet, the
type
name is actually the path to the
.aspx
file for the Page
, relative to
its directory context. When configuring other .NET classes one would
specify at minimum the fully qualified type name and the partial assembly
name.
In the case of the above example, those definitions are in the root
context ,so Login.aspx
and
Default.aspx
files also must be located in the root of
the web application's virtual directory. The master page is defined using
an absolute path because it could conceivably be referenced from child
contexts that are defined within subdirectories of the web
application.
The definitions for the Login
and
Default
pages do not specify either of the
id
and name
attributes, in marked
contrast to typical object definitions in Spring.NET, where the
id
or name
attributes are usually
mandatory (although not always, as in the case of inner object
definitions). In the case of Spring.Web manged Page
instances, one typically wants to use the name of the
.aspx
file name as the identifier. If an
id
is not specified, the Spring.Web infrastructure will
simply use the name of the .aspx
file as the object
identifier (minus any leading path information, and minus the file
extension too).
Nothing prevents an application developer from specifying an
id
or name
value explicitly;
explicit naming can be useful when, for example, one wants to expose the
same page multiple times using a slightly different configuration, such as
Add / Edit pages. To use abstract object definitions and have your page
inherit from them, use the name
attribute instead of
the id
attribute on the abstract object
definition.
Spring.Web also allows application developers to inject
dependencies into controls (both user controls and standard controls)
that are contained within a page. You can accomplish this globally for
all controls of a particular Type
by using the
location of the .ascx
as the object type identifier.
This process is similar to injecting into .aspx
pages, shown above.
<object type="~/controls/MyControl.ascx" abstract="true"> <!-- inject dependencies here... --> </object>
Note | |
---|---|
In either case, be sure to mark the object definition as
|
You can inject dependencies into custom HTTP modules by using the
class
Spring.Context.Support.HttpApplicationConfigurer
. You
register your custom HTTP module as you would normally; for example, a
module of the type HtmlCommentAppenderModule
, taken
from the Web Quick Start, appends additional comments into the http
response. It is registered as follows:
<httpModules> <add name="HtmlCommentAppender" type="HtmlCommentAppenderModule"/> </httpModules>
To configure this module, you use naming conventions to identify
the module name with configuration instructions in the Spring
configuration file. The ModuleTemplates
property of
HttpApplicationConfigurer is a dictionary that
takes as a key the name of the HTTP module, in this case
HtmlCommentAppender, and the Spring object
definition that describes how to perform dependency injection. The
object definition is in the standard <object/> style that you are
used to normally when configuring an object with Spring. An example is
shown below. HttpApplicationConfigurer' that configures the
HtmlCommentAppender's
AppendText property.
<object name="HttpApplicationConfigurer" type="Spring.Context.Support.HttpApplicationConfigurer, Spring.Web"> <property name="ModuleTemplates"> <dictionary> <entry key="HtmlCommentAppender"> <!-- this name must match the module name --> <object> <!-- select "view source" in your browser on any page to see the appended html comment --> <property name="AppendText" value="My configured comment!" /> </object> </entry> </dictionary> </property> </object>
You can see this example in action in the Web Quick Start.
Performing dependency injection on instances of
IHttpHandlers
and IHttpHandlerFactory
allows for a fully Spring-managed
<httpHandlers>
configuration section. To
perform dependency injection onan IHttpHandler
or
IHttpHandlerFactory,
register Spring's
MappingHandlerFactory
with a specific path or
wildcard string (that is, *.aspx) using the standard configuration of an
<httpHandler>
in web.config. For
example:
<system.web> <httpHandlers> <!-- the lines below map *any* request ending with *.ashx or *.whatever to the global(!) MappingHandlerFactory. Further "specialication" of which handler to map to is done within MappingHandlerFactory's configuration - use MappingHandlerFactoryConfigurer for this (see below) --> <add verb="*" path="*.ashx" type="Spring.Web.Support.MappingHandlerFactory, Spring.Web" validate="true"/> <add verb="*" path="*.whatever" type="Spring.Web.Support.MappingHandlerFactory, Spring.Web" validate="false"/> </httpHandlers> </system.web>
Spring's MappingHandlerFactory serves a layer of indirection so that you can configure multiple handler mappings with Spring. You do this by configuring a IDictionary HandlerMap property on the class MappingHandlerFactoryConfigurer. The dictionary key is a regular expression that matches the request URL, and the value is a reference to the name of a Spring managed instance of an IHttpHandler or IHttpHandlerFactory . The Spring managed instance is configured via dependency injection using the standard <object/> XML configuraiton schema.
The configuration of MappingHandlerFactoryConfigurer is shown:
<objects xmlns="http://www.springframework.net"> <!-- configures the global GenericHandlerFactory instance --> <object name="mappingHandlerFactoryConfigurer" type="Spring.Web.Support.MappingHandlerFactoryConfigurer, Spring.Web"> <property name="HandlerMap"> <dictionary> <!-- map any request ending with *.whatever to NoOpHandler --> <entry key="\.whatever$" value="myCustomHandler" /> <entry key="\.ashx$" value="standardHandlerFactory" /> </dictionary> </property> </object> <object name="standardHandlerFactory" type="Spring.Web.Support.DefaultHandlerFactory, Spring.Web" /> <!-- defines a standard singleton that will handle *.whatever requests --> <object name="myCustomHandler" type="MyCustomHttpHandler, App_Code"> <property name="MessageText" value="This text is injected via Spring" /> </object> <!-- used for configuring ~/DemoHandler.ashx custom handler note, that this is an abstract definition because 'type' is not specified --> <object name="DemoHandler.ashx"> <property name="OutputText"> <value>This text is injected via Spring</value> </property> </object> </objects>
Spring's DefaultHandlerFactory
uses the .NET
class System.Web.UI.SimpleHandlerFactory to
create handler instances and configures each instance by using an object
definition whose name matches the request URL's filename. The abstract
object definition of DemoHandler.ashx
is an example
of this approach. You can also configure standard classes that implement
the IHttpHandler interface as demonstrated in the
example above for the class
MyCustomHttpHandler
.
Refer to the Web Quick Start application too see this in action.
Custom providers can be configured via dependency injection with Spring. The approach to configuration for providers is to use a family of adapters that correspond 1-to-1 with the standard ASP.NET providers that are registered via the standard ASP.NET mechanism. The adapters inherit from their correspondingly named provider class in the .NET class library.
MembershipProviderAdapter
ProfileProviderAdapter
RoleProviderAdapter
SiteMapProviderAdapter
Here is an example of how to register the adapter for membership providers.
<membership defaultProvider="mySqlMembershipProvider"> <providers> <clear/> <add connectionStringName="" name="mySqlMembershipProvider" type="Spring.Web.Providers.MembershipProviderAdapter, Spring.Web"/> </providers> </membership>
The name of the provider must match the name of the object in the Spring configuration that will serve as the actual provider implementation. Configurable versions of the providers are found in ASP.NET so that you can use the full functionality of Spring to configure these standard provider implementations, by using property placeholders, and so on. The providers are:
ConfigurableActiveDirectoryMembershipProvider
ConfigurableSqlMembershipProvider
ConfigurableSqlProfileProvider
ConfigurableSqlRoleProvider
ConfigurableXmlSiteMapProvider
This example configuration taken from the Web Quick Start application sets the description property and connection string.
<object id="mySqlMembershipProvider" type="Spring.Web.Providers.ConfigurableSqlMembershipProvider"> <property name="connectionStringName" value="MyLocalSQLServer" /> <property name="parameters"> <name-values> <add key="description" value="membershipprovider description" /> </name-values> </property> </object>
Your own custom providers of course will contain additional configuration specific to your implementation.
You may need to customize Spring.Web's dependency injection processing, such as when using GridViews or other complex 3rd party custom controls. Often these controls are not configured using dependency injection but Spring considers each control and its nested child controls as candidates for DI. With very large (>1000) nested controls that candidate evaluation process can unecessarily slow down your page. To address this problem, you can tell Spring to not attempt to configure via DI the sections of your page that contain these controls or implementing the interface ISupportsWebDependencyInjection and explicitly ask Spring to inject dependencies on a particular contorol. These approaches are shown below
[C#] class MyControl : Control, ISupportsWebDependencyInjection { private IApplicationContext _defaultApplicationContext; public IApplicationContext DefaultApplicationContext { get { return _defaultApplicationContext; } set { _defaultApplicationContext = value; } } override protected AddedControl( Control control, int index ) { // handle DI for children ourselves - // defaults to a call to InjectDependenciesRecursive WebUtils.InjectDependenciesRecursive( _defaultApplicationContext, control ); base.AddedControl( control, index ); } }
A Spring server control, Panel, provides an easier way to turn off dependency injection for parts of your page:
<spring:Panel runat="server" suppressDependencyInjection="true" renderContainerTag="false"> .. put your heavy controls here - they won't be touched by DI </spring:Panel>
By wrapping the performance-sensitive parts of your page within this panel, you can easily turn off DI by setting the attribute suppressDependencyInjection to true. By default <spring:Panel/> will not render a container tag (<div>, <span>, and so on). You can modify this behavior by setting the attribute renderContainerTag accordingly.
Spring.NET web applications support an additional attribute within object definition elements that allows you to control the scope of an object:
<object id="myObject" type="MyType, MyAssembly" scope="application | session | request"/>
Possible
values for the scope attribute are application,
session, and request
. Application
scope is the default, and is used for all objects with an undefined scope
attribute. This scope creates a single instance of an object for the
duration of the IIS application, so that the objects works exactly like
the standard singleton objects in non-web applications. Session scope
defines objects so that an instance is created for each HttpSession. This
scope is ideal for objects such as user profile, shopping cart, and so on
that you want bound to a single user.
Request scope creates one instance per HTTP request. Unlike calls to
prototype objects, calls to
IApplicationContext.GetObject
return the same instance
of the request-scoped object during a single HTTP request. This allows
you, for example, to inject the same request-scoped object into multiple
pages and then use server-side transfer to move from one page to another.
As all the pages are executed within the single HTTP request in this case,
they share the same instance of the injected object.
Objects can only reference other objects that are in the same or broader scope. This means that application-scoped objects can only reference other application-scoped objects, session-scoped objects can reference both session and application-scoped objects, and request-scoped objects can reference other request-, session-, or application-scoped objects. Also, prototype objects (including all ASP.NET web pages defined within Spring.NET context) can reference singleton objects from any scope, as well as other prototype objects.
Support for ASP.NET 1.1 master pages in Spring.Web is very similar to the support for master pages in ASP.NET 2.0.
A web developer can define a layout template for the site as a
master page and specify content placeholders that other pages can then
reference and populate. A sample master page
(MasterLayout.ascx
) could look like this:
<%@ Control language="c#" Codebehind="MasterLayout.ascx.cs" AutoEventWireup="false" Inherits="MyApp.Web.UI.MasterLyout" %> <%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <html> <head> <title>Master Page</title> <link rel="stylesheet" type="text/css" href="<%= Context.Request.ApplicationPath %>/css/styles.css"> <spring:ContentPlaceHolder id="head" runat="server"/> </head> <body> <form runat="server"> <table cellPadding="3" width="100%" border="1"> <tr> <td colspan="2"> <spring:ContentPlaceHolder id="title" runat="server"> <!-- default title content --> </spring:ContentPlaceHolder> </td> </tr> <tr> <td> <spring:ContentPlaceHolder id="leftSidebar" runat="server"> <!-- default left side content --> </spring:ContentPlaceHolder> </td> <td> <spring:ContentPlaceHolder id="main" runat="server"> <!-- default main area content --> </spring:ContentPlaceHolder> </td> </tr> </table> </form> </body> </html>
In the preceding code, the master page defines the overall layout for the page, in addition to four content placeholders that other pages can override. The master page can also include default content within the placeholder that will be displayed if a derived page does not override the placeholder.
A page that uses this master page (Child.aspx) might look like this:
<%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %> <%@ Page language="c#" Codebehind="Child.aspx.cs" AutoEventWireup="false" Inherits="ArtFair.Web.UI.Forms.Child" %> <html> <body> <spring:Content id="leftSidebarContent" contentPlaceholderId="leftSidebar" runat="server"> <!-- left sidebar content --> </spring:Content> <spring:Content id="mainContent" contentPlaceholderId="main" runat="server"> <!-- main area content --> </spring:Content> </body> </html>
The <spring:Content/>
control in the
example uses the contentPlaceholderId
attribute
(property) to specify exactly which placeholder from the master page is to
be overridden. Because this particular page does not define content
elements for the head and title placeholders, the content elements are
defined by the default content supplied in the master page.
Both the ContentPlaceHolder
and
Content
controls can contain any valid ASP.NET markup:
HTML, standard ASP.NET controls, user controls, and so on.
Tip | |
---|---|
Technically, the |
The Spring.Web.UI.Page
class exposes a property
called MasterPageFile
, which you can use to specify
the master page.
The recommended way to do this is by leveraging the Spring.NET IoC container and creating definitions similar to the following:
<?xml version="1.0" encoding="utf-8" ?> <objects xmlns="http://www.springframework.net"> <object name="basePage" abstract="true"> <property name="MasterPageFile" value="~/MasterLayout.ascx"/> </object> <object type="Child.aspx" parent="basePage"> <!-- inject other objects that page needs --> </object> </objects>
This approach allows application developers to change the master
page for a number of pages within a web application. You can still
override the master page on a per context or per page basis by creating
a new abstract page definition within a child context, or by specifying
the MasterPageFile
property directly.
The existing data binding support in ASP.NET is one-way only. It allows application developers to bind page controls to the data model and display information from the data model, but it does not permit the extraction of values from the controls when the form is submitted. Spring.Web adds bidirectional data binding to ASP.NET by allowing developers to specify data binding rules for their page, and by automatically evaluating configured data binding rules at the appropriate time in the page's lifecycle.
ASP.NET does support model management within the postbacks. It has a ViewState management, but that takes care of the control state only and does not address the state of any presentation model objects to which these controls are bound. To manage a model within ASP.NET, developers typically use an HTTP session object to store the model between the postbacks. This process results in boilerplate code that can and should be eliminated, which is exactly what Spring.Web does by providing a simple set of model management methods.
To take advantage of the bidirectional data binding and model
management support provided by Spring.Web, you will
have to couple your presentation layer to Spring.Web; this is because
features require you to extend a
Spring.Web.UI.Page
instead of the usual
System.Web.UI.Page
class.
Spring.Web data binding is very easy to use. Simply override the
protected InitializeDataBindings
method and configure
data binding rules for the page. You also need to override three model
management methods: InitializeModel
,
LoadModel
and SaveModel
. This
process is illustrated by an example from the SpringAir reference
application. First, take a look at the page markup:
<%@ Page Language="c#" Inherits="TripForm" CodeFile="TripForm.aspx.cs" %> <asp:Content ID="body" ContentPlaceHolderID="body" runat="server"> <div style="text-align: center"> <h4><asp:Label ID="caption" runat="server"></asp:Label></h4> <table> <tr class="formLabel"> <td> </td> <td colspan="3"> <spring:RadioButtonGroup ID="tripMode" runat="server"> <asp:RadioButton ID="OneWay" runat="server" /> <asp:RadioButton ID="RoundTrip" runat="server" /> </spring:RadioButtonGroup> </td> </tr> <tr> <td class="formLabel" align="right"> <asp:Label ID="leavingFrom" runat="server" /></td> <td nowrap="nowrap"> <asp:DropDownList ID="leavingFromAirportCode" runat="server" /> </td> <td class="formLabel" align="right"> <asp:Label ID="goingTo" runat="server" /></td> <td nowrap="nowrap"> <asp:DropDownList ID="goingToAirportCode" runat="server" /> </td> </tr> <tr> <td class="formLabel" align="right"> <asp:Label ID="leavingOn" runat="server" /></td> <td nowrap="nowrap"> <spring:Calendar ID="departureDate" runat="server" Width="75px" AllowEditing="true" Skin="system" /> </td> <td class="formLabel" align="right"> <asp:Label ID="returningOn" runat="server" /></td> <td nowrap="nowrap"> <div id="returningOnCalendar"> <spring:Calendar ID="returnDate" runat="server" Width="75px" AllowEditing="true" Skin="system" /> </div> </td> </tr> <tr> <td class="buttonBar" colspan="4"> <br/> <asp:Button ID="findFlights" runat="server"/></td> </tr> </table> </div> </asp:Content>
Ignore for the moment the fact that none of the label
controls have text defined; defining label controls is described later
when we discuss localization in Spring.NET. For the purposes of the
current discussion, a number of input controls are defined:
tripMode
radio button group,
leavingFromAirportCode
and
goingToAirportCode
dropdown lists, as well as two
Spring.NET Calendar controls, departureDate
and
returnDate
.
Take a look at the model to which you bind the form:
namespace SpringAir.Domain { [Serializable] public class Trip { // fields private TripMode mode; private TripPoint startingFrom; private TripPoint returningFrom; // constructors public Trip() { this.mode = TripMode.RoundTrip; this.startingFrom = new TripPoint(); this.returningFrom = new TripPoint(); } public Trip(TripMode mode, TripPoint startingFrom, TripPoint returningFrom) { this.mode = mode; this.startingFrom = startingFrom; this.returningFrom = returningFrom; } // properties public TripMode Mode { get { return this.mode; } set { this.mode = value; } } public TripPoint StartingFrom { get { return this.startingFrom; } set { this.startingFrom = value; } } public TripPoint ReturningFrom { get { return this.returningFrom; } set { this.returningFrom = value; } } } [Serializable] public class TripPoint { // fields private string airportCode; private DateTime date; // constructors public TripPoint() {} public TripPoint(string airportCode, DateTime date) { this.airportCode = airportCode; this.date = date; } // properties public string AirportCode { get { return this.airportCode; } set { this.airportCode = value; } } public DateTime Date { get { return this.date; } set { this.date = value; } } } [Serializable] public enum TripMode { OneWay, RoundTrip } }
As you can see, Trip
class uses the
TripPoint
class to represent departure and return,
which are exposed as StartingFrom
and
ReturningFrom
properties. It also uses
TripMode
enumeration to specify whether the trip is one
way or return trip, which is exposed as Mode
property.
Here is the code-behind class that ties everything together:
public class TripForm : Spring.Web.UI.Page { // model private Trip trip; public Trip Trip { get { return trip; } set { trip = value; } } // service dependency, injected by Spring IoC container private IBookingAgent bookingAgent; public IBookingAgent BookingAgent { set { bookingAgent = value; } } // model management methods protected override void InitializeModel() { trip = new Trip(); trip.Mode = TripMode.RoundTrip; trip.StartingFrom.Date = DateTime.Today; trip.ReturningFrom.Date = DateTime.Today.AddDays(1); } protected override void LoadModel(object savedModel) { trip = (Trip) savedModel; } protected override object SaveModel() { return trip; } // data binding rules protected override void InitializeDataBindings() { BindingManager.AddBinding("tripMode.Value", "Trip.Mode"); BindingManager.AddBinding("leavingFromAirportCode.SelectedValue", "Trip.StartingFrom.AirportCode"); BindingManager.AddBinding("goingToAirportCode.SelectedValue", "Trip.ReturningFrom.AirportCode"); BindingManager.AddBinding("departureDate.SelectedDate", "Trip.StartingFrom.Date"); BindingManager.AddBinding("returnDate.SelectedDate", "Trip.ReturningFrom.Date"); } // event handler for findFlights button, uses injected 'bookingAgent' // service and model 'trip' object to find flights private void SearchForFlights(object sender, EventArgs e) { FlightSuggestions suggestions = bookingAgent.SuggestFlights(trip); if (suggestions.HasOutboundFlights) { // redirect to SuggestedFlights page } } }
Note the following about the three preceding pieces of code:
When the page is initially loaded (IsPostback ==
false
), the InitializeModel
() method is
called, which initializes the trip object by creating a new instance
and setting its properties to desired values. Right before the page
is rendered, the SaveModel
() method is invoked,
and the value it returns is stored within the HTTP session. On each
postback, the LoadModel()
method is called, and
the value returned by the previous call to
SaveModel
is passed to SaveModel
as
an argument.
In this particular case the implementation is very simple
because our whole model is just the trip
object.
As such, SaveModel
() simply returns the
trip
object, and LoadModel()
casts the SaveModel
() argument to
Trip
and assigns it to the
trip
field within the page. In more complex
scenarios, the SaveModel() method will typically return a dictionary
that contains your model objects. Those values will be read from the
dictionary within the LoadModel() method.
InitializeDataBindings
method defines the
binding rules for all of the five input controls on the form. The
controls are represented by the variables tripMode,
leavingFromAirportCode, goingToAirportCode, departueDate, and
returnDate. The binding rules are created by invoking the
AddBinding
method on the
BindingManager
exposed by the page. The
AddBinding
method is heavily overloaded and it allows you
to specify a binding direction and a
formatter to use in addition to the
source and target binding expressions that are
used above. These optional parameters are discussed later in this
chapter. For now, focus on the source and target expressions.
The Spring.NET data binding framework uses Spring.NET
Expression Language to define binding expressions. In most cases, as
in the example above, both source and target expression will
evaluate to a property or a field within one of the controls or a
data model. This is always the case when you are setting a
bidirectional binding, as both binding expressions need to be
"settable". The InitializeDataBindings
method is
executed only once per page type. Basically,
all binding expressions are parsed the first time the page is
instantiated, and are then cached and used by all instances of that
same page type that are created at a later time. This is done for
performance reasons, as data binding expression parsing on every
postback is unnecessary and would add a significant overhead to the
overall page processing time.
Notice that the SearchForFlights
event handler
has no dependencies on the view elements. It simply uses the
injected bookingAgent service and a trip object in order to obtain a
list of suggested flights. Furthermore, if you make any
modifications to the trip object within your event handler, bound
controls are updated accordingly just before the page is
rendered.
Note | |
---|---|
The lack of view elements in the event handler accomplishes one of the major goals we set out to achieve, allowing developers to remove view element references from the page event handlers and decouple controller-type methods from the view. |
This section describes how data binding is actually implemented, the extension points, and additional features that make the data binding framework usable in real-world applications.
The Spring.NET data binding framework revolves around two main
interfaces: IBinding
and
IBindingContainer
. The IBinding
interface is definitely the more important one of the two, as it has to
be implemented by all binding types. This interface defines several
methods, with some of them being overloaded for
convenience:
public interface IBinding { void BindSourceToTarget(object source, object target, ValidationErrors validationErrors); void BindSourceToTarget(object source, object target, ValidationErrors validationErrors, IDictionary variables); void BindTargetToSource(object source, object target, ValidationErrors validationErrors); void BindTargetToSource(object source, object target, ValidationErrors validationErrors, IDictionary variables); void SetErrorMessage(string messageId, params string[] errorProviders); }
The BindSourceToTarget
method is used to
extract and copy bound values from the source object to the target
object, and BindTargetToSource
does the opposite.
Both method names and parameter types are generic because the data
binding framework can be used to bind any two objects. Using it to bind
web forms to model objects is just one of its possible uses, although a
very common one and tightly integrated into the Spring.NET Web
Framework.
The ValidationErrors
parameter requires further
explanation. Although the data binding framework is not in any way
coupled to the data validation framework, they are in some ways related.
For example, while the data validation framework is best suited to
validate the populated model according to the business rules, the data
binding framework is in a better position to validate data types during
the binding process. However, regardless of where specific validation is
performed, all error messages should be presented to the user in a
consistent manner. In order to accomplish this, Spring.NET Web Framework
passes the same ValidationErrors
instance to binding
methods and to any validators that might be executed within your event
handlers. This process ensures that all error messages are stored
together and are displayed consistently to the end user, using
Spring.NET validation error controls.
The last method in the IBinding
interface,
SetErrorMessage
, enables you to specify the resource
id of the error message to be displayed in case of binding error, as
well as a list of strings, that server as identifiers to tag error
messages for the purposes of linking specific error messages to
locations in the page markup. We wil see an example of the
SetErrorMessage
usage in a later section.
The IBindingContainer
interface extends the
IBinding
interface and adds the following
members:
public interface IBindingContainer : IBinding { bool HasBindings { get; } IBinding AddBinding(IBinding binding); IBinding AddBinding(string sourceExpression, string targetExpression); IBinding AddBinding(string sourceExpression, string targetExpression, BindingDirection direction); IBinding AddBinding(string sourceExpression, string targetExpression, IFormatter formatter); IBinding AddBinding(string sourceExpression, string targetExpression, BindingDirection direction, IFormatter formatter); }
The IBindingContainer
interface has several
overloaded AddBinding
methods.
AddBinding(IBinding binding)
is the most generic one,
as it can be used to add any binding type to the container. The other
four are convenience methods that provide a simple way to add the most
commonly used implementation of the
IBinding interface,
SimpleExpressionBinding
. The
SimpleExpressionBinding
was used under the covers in
the example at the beginning of this section to bind our web form to a
Trip
instance when calling methods on the property
BindingManager. Note that the
BindingManager property is of the type
IBindingContainer.
SimpleExpressionBinding uses Spring.NET
Expression Language (SpEL) to extract and to set values within source
and target objects.
In the TripForm example, the configuration of the BindingManager
shows the basic usage of how SpEL can be used to specify a
sourceExpression
and
targetExpression
arguments. This code section is
repeated below
protected override void InitializeDataBindings() { BindingManager.AddBinding("tripMode.Value", "Trip.Mode"); BindingManager.AddBinding("leavingFromAirportCode.SelectedValue", "Trip.StartingFrom.AirportCode"); BindingManager.AddBinding("goingToAirportCode.SelectedValue", "Trip.ReturningFrom.AirportCode"); BindingManager.AddBinding("departureDate.SelectedDate", "Trip.StartingFrom.Date"); BindingManager.AddBinding("returnDate.SelectedDate", "Trip.ReturningFrom.Date"); }
In this case, the first argument is a
sourceExpression
evaluated in the context of the page
itself. The sourceExpression
'tripMode.Value'
represents the value in the HTML control and
the targetExpression "Trip.Mode"
represents the value
it will be mapped onto whent the page is rendered. When the post-back
happens values from in "Trip.Mode"
get placed back
into the HTML control "tripMode.Value"
. This is a
common case in which bi-directional data mapping is symmetric in terms
of the sourceExpression
and
targetExpression
for both the initial rendering of
the page and when the post-back occurs. There other overloaded methods
that take BindingDirection
and
IFormatter
arguments are discussed in the next
section.
The direction
argument determines whether the
binding is bidirectional or unidirectional. By default, all data
bindings are bidirectional unless the direction argument is set to
either BindingDirection.SourceToTarget
or
BindingDirection.TargetToSource
. If one of these
values is specified, binding is evaluated only when the appropriate
Bind
Direction
method is invoked, and is completely ignored in the other direction.
This configuration is very useful when you want to bind some
information from the model into non-input controls, such as
labels.
However, unidirectional data bindings are also useful when your
form does not have a simple one-to-one mapping to a presentation
model. In the earlier trip form example, the presentation model was
intentionally designed to allow for simple one-to-one mappings. For
the sake of discussion, let's add the Airport
class
and modify our TripPoint
class as
follows:
namespace SpringAir.Domain { [Serializable] public class TripPoint { // fields private Airport airport; private DateTime date; // constructors public TripPoint() {} public TripPoint(Airport airport, DateTime date) { this.airport = airport; this.date = date; } // properties public Airport Airport { get { return this.airport; } set { this.airport = value; } } public DateTime Date { get { return this.date; } set { this.date = value; } } } [Serializable] public class Airport { // fields private string code; private string name; // properties public string Code { get { return this.code; } set { this.code = value; } } public string Name { get { return this.name; } set { this.name = value; } } } }
Instead of the string property AirportCode
, our
TripPoint
class now exposes an Airport
property of type Airport
, which is defined in the
preceding example. What was formerly a simple string-to-string
binding, with the airport code selected in a dropdown being copied
directly into the TripPoint.AirportCode
property and vice
versa, now becomes a not-so-simple string-to-Airport binding. So let's
see how we can solve this mismatch problem of converting a string to
an Airport instance and an Airport instance to a string.
Binding from the model to the control, namely the Airport to the string, is still very straightforward. You set up one-way bindings from the model to controls: The Model-To-Control is represented more generally by the enumeration, BindingDirection.TargetToSource.
protected override void InitializeDataBindings() { BindingManager.AddBinding("leavingFromAirportCode.SelectedValue", "Trip.StartingFrom.Airport.Code", BindingDirection.TargetToSource); BindingManager.AddBinding("goingToAirportCode.SelectedValue", "Trip.ReturningFrom.Airport.Code", BindingDirection.TargetToSource); ... }
You extract the airport code value from the
Trip.StartingFrom.Airport.Code
instead of
Trip.StartingFrom.AirportCode
since now the Code in
encapsulated inside the Airport class. Unfortunately, binding from the
control to the model the same way won't work, we need a way to create
an Airport instance from a string. Instead, you need to find an
instance of the Airport
class based on the airport
code and set the TripPoint.Airport
property to it.
Fortunately, Spring.NET data binding makes this simple, especially
because you already have airportDao
object defined
in the Spring context (see SpringAir Spring context configuration file
for details.). The AirportDao has a GetAirport(string
airportCode)
finder method. You set up data bindings from
source to target (control-to-model) that will invoke this finder
method when the page is submitted and the binding infrastructure maps
the sourceExpression onto the targetExpression.evaluating the source
expression.
Our complete set of bindings for these two drop-down lists will then look like this:
protected override void InitializeDataBindings() { BindingManager.AddBinding("@(airportDao).GetAirport(leavingFromAirportCode.SelectedValue)", "Trip.StartingFrom.Airport", BindingDirection.SourceToTarget); BindingManager.AddBinding("leavingFromAirportCode.SelectedValue", "Trip.StartingFrom.Airport.Code", BindingDirection.TargetToSource); BindingManager.AddBinding("@(airportDao).GetAirport(goingToAirportCode.SelectedValue)", "Trip.ReturningFrom.Airport", BindingDirection.SourceToTarget); BindingManager.AddBinding("goingToAirportCode.SelectedValue", "Trip.ReturningFrom.Airport.Code", BindingDirection.TargetToSource); ... }
By using a pair of bindings for each control, one for each direction and using SpEL's feature to reference objects defined in the Spring context, you can resolve this data binding issue.
The last overloaded methods of
IBindingContainer we need to discuss
are those that take a IFormatter
argument. is an
argument to the AddBinding
method. This argument
allows you to specify a formatter that you use to parse string value
from the input control before it is bound to the model, and to format
strongly typed model value before the model is bound to the
control.
You typically use one of the formatters provided in the Spring.Globalization.Formatters namespace, but if your requirements cannot be satisfied by a standard formatter, you can write your own by implementing a simple IFormatter interface:
public interface IFormatter { string Format(object value); object Parse(string value); }
Standard formatters provided with Spring.NET are:
CurrencyFormatter
,
DateTimeFormatter
,
FloatFormatter
,
IntegerFormatter
,
NumberFormatter
and
PercentFormatter
, which are sufficient for most
usage scenarios.
Because the data binding framework uses the same expression evaluation engine as the Spring.NET IoC container, it uses any registered type converters to perform data binding. Many type converters are included with Spring.NET (take a look at the classes in Spring.Objects.TypeConverters namespace) and are automatically registered for you, but you can implement your own custom converters and register them by using standard Spring.NET type converter registration mechanisms.
Spring.Web's base Page
class adds two events
to the standard .NET page lifecycle: DataBound
and
DataUnbound
.
You can register for an DataUnbound
event
which will be fired after the data model is updated with values from
the controls. Specifically, in terms of the Page lifecycle, it is
fired right after the Load
event and only on
postbacks, because it not make sense to update the data model with the
controls' initial values.
The DataBound
event is fired after controls
are updated with values from the data model. This event occurs right
before the PreRender
event.
The fact that the data model is updated immediately after the
Load
event and that controls are updated right
before the PreRender
event means that your event
handlers can work with a correctly updated data model, as they execute
after the Load
event, and that any changes you make
to the data model within event handlers are reflected in the controls
immediately afterwards, as the controls are updated prior to the
actual rendering.
If errors occur in the databinding (for example, in trying to bind a string 'hello' to an integer property on the model), you can specify how those fundamental binding errors should be rendered. The following snippet is from the Web Quick Start 'RobustEmployeeInfo' example:
[Default.aspx.cs] protected override void InitializeDataBindings() { // collect txtId.Text binding errors in "id.errors" collection BindingManager.AddBinding("txtId.Text", "Employee.Id").SetErrorMessage("ID has to be an integer", "id.errors"); ... [Default.aspx] ... <asp:TextBox ID="txtId" runat="server" /> <!-- output validation errors from "id.errors" collection --> <spring:ValidationError Provider="id.errors" runat="server" /> ...
The SetErrorMessage specifies the message text or resource id of the error message to be displayed. This is followed by a a variable length list of strings that serve to as a means to assign a friendly name to associate with this error should it occur. The same 'tag', or error provider name, can be used across different calls to 'AddBinding'. This is commonly the case if you want to present several errors together in the page. In the preceding example, the 'tag' or error provider name is "id.errors" will be rendered in Spring's ValidationError User Control, for example as shown below in this fragment of page markup. Validation controls are discussed more extensively in this section.
<td> <asp:TextBox ID="txtId" runat="server" EnableViewState="false" /> <spring:ValidationError ID="errId" Provider="id.errors" runat="server" /><!-- read msg from "id.error" provider --> </td>
HttpRequestListBindingContainer
extracts posted raw values from the request and populates
the specified IList by creating objects of the type specified and
populating each object according to the requestBindings
collection.
Please check out the Web Quick Start sample's demo of HttpRequestListBindingContainer. Below is an exerpt from that example showing how to use a HttpRequestListBindingContainer.
protected override void InitializeDataBindings() { // HttpRequestListBindingContainer unbinds specified values from Request -> Productlist HttpRequestListBindingContainer requestBindings = new HttpRequestListBindingContainer("sku,name,quantity,price", "Products", typeof(ProductInfo)); requestBindings.AddBinding("sku", "Sku"); requestBindings.AddBinding("name", "Name"); requestBindings.AddBinding("quantity", "Quantity", quantityFormatter); requestBindings.AddBinding("price", "Price", priceFormatter); BindingManager.AddBinding(requestBindings); }
Note | |
---|---|
Because browsers do not send the values of unchecked checkboxes, you cannot use HttpRequestListBindingContainer with <input type="checkbox" > html controls. |
To simplify use of Spring's Data Binding feature on web pages and controls, Spring.Web provides a special DataBindingPanel container control. A DataBindingPanel does not render any html code itself, but allows you to define additional, data binding-related attributes for its child controls.
<%@ Page Language="C#" CodeFile="Default.aspx.cs" Inherits="DataBinding_EasyEmployeeInfo_Default" %> <%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %> <html> <body> <spring:DataBindingPanel ID="ctlDataBindingPanel" runat="server"> <table cellpadding="3" cellspacing="3" border="0"> <tr> <td>Employee ID:</td> <td> <asp:TextBox ID="txtId" runat="server" BindingTarget="Employee.Id" /> </td> </tr> <tr> <td>First Name:</td> <td><asp:TextBox ID="txtFirstName" runat="server" BindingTarget="Employee.FirstName" /></td> </tr> </table> </spring.DataBindingPanel> </body> </html>
Using DataBindingPanel, you can specify the binding information directly on the control declaration. The following attributes are recognized by a DataBindingPanel:
BindingTarget
corresponds to the target
expression used in IBindingContainer.AddBinding().
BindingSource
corresponds to the source
expression used in IBindingContainer.AddBinding(). For standard
controls you don't need to specify the source expression. If you are
binding to some custom control, of course you must specific this
attribute.
BindingDirection
is one of the values of the
BindingDirection enumeration.
BindingFormatter
is the object name of a custom
formatter. The formatter instance is obtained by a call to
IApplicationContext.GetObject() each time it is needed.
BindingType
is the type of a completely
customized binding. Note that a custom binding type must implement
the following constructor signature:
ctor(string source,string target, BindingDirection,
IFormatter)
Note | |
---|---|
The Visual Studio Web Form Editor complains about binding attributes because it does not recognize them. You can safely ignore those warnings. |
As mentioned in the chapter introduction, model management needs
an application developer to override
InitializeModel()
, SaveModel()
and
LoadModel()
in order to store model information
between requests in the user's session. On web farms, storing
information in a user's session is not a good strategy. You can choose
another persistence strategy by setting the
ModelPersistenceMedium property on Spring's base
Page or UserContorl class (e.g.
Spring.Web.UI.UserControl)
<object id="modelPersister" type="Sample.DatabaseModelPersistenceMedium, MyCode"/> <object type="UserRegistration.aspx"> <property name="ModelPersistenceMedium" ref="modelPersister"/> </object>
To implement any arbitrary persistence strategy, implement the IModelPersistenceMedium interface:
public interface IModelPersistenceMedium { // Load the model for the specified control context. object LoadFromMedium( Control context ); // Save the specified model object. void SaveToMedium( Control context, object modelToSave ); }
Although the .NET framework has excellent localization support, the support within ASP.NET 1.x is incomplete. Spring provides support for localization in ASP.NET 1.1 apps in the manner of ASP.NET 2.0. Despite the initial focus on righer localization for ASP.NET 1.1 applications, using Spring's localization features in ASP.NET 2.0 or higher applications does provide some useful additional features with a similar programming model, such as image localization, push mechansims, and built-in support for user culture management via various mechansims.
Every .aspx
page in an ASP.NET project has a
resource file associated with it, but those resources are never used by
the current ASP.NET infrastructure). ASP.NET 2.0 changes this and allow
application developers to use local resources for pages. In the meantime,
the Spring.NET team built in to Spring.Web support for using local pages
resources, thus allowing ASP.NET 1.1 application developers to using
ASP.NET 2.0-like page resources.
Spring.Web supports several different approaches to localization within a web application, which can be mixed and matched as appropriate. You can use push and pull mechanisms, as well as globally defined resources when a local resource cannot be found. Spring.Web also supports user culture management and image localization, which are described in later sections.
Tip | |
---|---|
For introductory information covering ASP.NET globalization and localization, see Globalization Architecture for ASP.NET and Localization Practices for ASP.NET 2.0 by Michele Leroux Bustamante. |
A localizer is an object that implements the
Spring.Globalization.ILocalizer
interface.
Spring.Globalization.AbstractLocalizer
is a
convenient base class for localization: this class has one abstract
method, LoadResources
. This method must load and
return a list of all resources that must be automatically applied from
the resource store.
To apply resources automatically, a localizer needs to be injected
into all pages that require automatic resource application. You
typically accomplish configuration using dependency injection of a page
base page definition that other page definitions will inherit from. The
injected localizer inspects the resource file when the page is first
requested, caches the resources that start with the
'$this'
marker string value, and applies the values
to the controls that populate the page prior to the page being
rendered.
Spring.NET ships with one concrete implementation of a localizer,
Spring.Globalization.Localizers.ResourceSetLocalizer
,
that retrieves a list of resources to apply from the local resource
file. Future releases of Spring.NET may provide other localizers that
read resources from an XML file or even from a flat text file that
contains resource name-value pairs that allow application developers to
store resources within the files in a web application instead of as
embedded resources in an assembly. Of course, if an application
developer prefers to store such resources in a database, the developer
can write a custom ILocalizer
implementation that
loads a list of resources to apply from a database.
You typically configure the localizer to be used within an abstract base definition for those pages that require localization:
<object id="localizer" type="Spring.Globalization.Localizers.ResourceSetLocalizer, Spring.Core"/> <object name="basePage" abstract="true"> <description> Pages that reference this definition as their parent (see examples below) will automatically inherit following properties. </description> <property name="Localizer" ref="localizer"/> </object>
Of course, nothing prevents an application developer from defining a different localizer for each page in the application; in any case, one can always override the localizer defined in a base (page) definition. Alternatively, if one does want any resources to be applied automatically one can completely omit the localizer definition.
One last thing to note is that Spring.NET
UserControl
instances will (by default) inherit the
localizer and other localization settings from the page that they are
contained within, but one can similarly also override that behavior
using explicit dependency injection.
With push localization, an application developer specifies
localization resources in the resource file for the page, and the
framework automatically applies those resources to the user controls on
the page. For example, an application developer could define a page such
as UserRegistration.aspx
:
<%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %> <%@ Page language="c#" Codebehind="UserRegistration.aspx.cs" AutoEventWireup="false" Inherits="ArtFair.Web.UI.Forms.UserRegistration" %> <html> <body> <spring:Content id="mainContent" contentPlaceholderId="main" runat="server"> <div align="right"> <asp:LinkButton ID="english" Runat="server" CommandArgument="en-US">English</asp:LinkButton> <asp:LinkButton ID="serbian" Runat="server" CommandArgument="sr-SP-Latn">Srpski</asp:LinkButton> </div> <table> <tr> <td><asp:Label id="emailLabel" Runat="server"/></td> <td><asp:TextBox id="email" Runat="server" Width="150px"/></td> </tr> <tr> <td><asp:Label id="passwordLabel" Runat="server"/></td> <td><asp:TextBox id="password" Runat="server" Width="150px"/></td> </tr> <tr> <td><asp:Label id="passwordConfirmationLabel" Runat="server"/></td> <td><asp:TextBox id="passwordConfirmation" Runat="server" Width="150px"/></td> </tr> <tr> <td><asp:Label id="nameLabel" Runat="server"/></td> <td><asp:TextBox id="name" Runat="server" Width="150px"/></td> </tr> ... <tr> <td colspan="2"> <asp:Button id="saveButton" Runat="server"/> <asp:Button id="cancelButton" Runat="server"/> </td> </tr> </table> </spring:Content> </body> </html>
In the preceding .aspx
code, none of the
Label
or Button
controls have had
a value assigned to the Text
property. The values of
the Text
property for these controls are stored in
the local resource file (of the page) using the following convention to
identify the resource (string).
$this.controlId.propertyName
The corresponding local resource file,
UserRegistration.aspx.resx
, is shown below.
<root> <data name="$this.emailLabel.Text"> <value>Email:</value> </data> <data name="$this.passwordLabel.Text"> <value>Password:</value> </data> <data name="$this.passwordConfirmationLabel.Text"> <value>Confirm password:</value> </data> <data name="$this.nameLabel.Text"> <value>Full name:</value> </data> ... <data name="$this.countryLabel.Text"> <value>Country:</value> </data> <data name="$this.saveButton.Text"> <value>$messageSource.save</value> </data> <data name="$this.cancelButton.Text"> <value>$messageSource.cancel</value> </data> </root>
Viewing .resx file in Visual Studio 2003 | |
---|---|
To view the .resx file for a page, you may need to enable "Project/Show All Files" in Visual Studio. When "Show All Files" is enabled, the .resx file appears like a "child" of the code-behind page. When Visual Studio creates the .resx file, it includes an
There is no way to visually edit resources in a RESX file. Lutz Roeder has created a tool named Resourcer that you can use to edit them |
Creating a .resx file in Visual Studio 2005/8 | |
---|---|
To create a resource file in VS 2005, open your control or page in design mode and select "Tools/Generate local resource" from the menu. |
You must create a localizer for the page to enable automatic localization:
<object id="localizer" type="Spring.Globalization.Localizers.ResourceSetLocalizer, Spring.Core"/> <object type="UserRegistration.aspx"> <property name="Localizer" ref="localizer"/> </object>
For more information on configuring localizers see Section 22.8.1, “Working with localizers”
Two resource definitions from the previous section require some additional explanation:
<data name="$this.saveButton.Text"> <value>$messageSource.save</value> </data> <data name="$this.cancelButton.Text"> <value>$messageSource.cancel</value> </data>
In some cases it makes sense to apply a resource that is defined
globally as opposed to locally. In this example, it
makes better sense to define values for the Save
and
Cancel
buttons globally as they will probably be used
throughout the application.
The above example demonstrates how one can achieve that by defining a resource redirection expression as the value of a local resource by prefixing a global resource name with the following string.
$messageSource.
In the preceding example, this string tells the localizer to use
the save
and cancel
portions of
the resource key as lookup keys to retrieve the actual values from a
global message source. You need to define a resource redirect only
once, typically in the invariant resource file. Any lookup for a
resource redirect falls back to the invariant culture, and results in a
global message source lookup using the correct culture.
Global resources are (on a per-context basis) defined as a plain
vanilla object definition using the reserved name of
messageSource
, which you can add to your Spring.NET
configuration file:
<object id="messageSource" type="Spring.Context.Support.ResourceSetMessageSource, Spring.Core"> <property name="ResourceManagers"> <list> <value>MyApp.Web.Resources.Strings, MyApp.Web</value> </list> </property> </object>
for .NET 2.0 or higher | |
---|---|
To use resources from your App_GlobalResources folder, specify
<value>Resources.Strings,
App_GlobalResources</value>
|
See the SpringAir example application for more. The global
resources are cached within the Spring.NET
IApplicationContext
and are accessible through the
Spring.NET IMessageSource
interface.
The Spring.Web Page
and
UserControl
classes have a reference to their owning
IApplicationContext
and its associated
IMessageSource
. As such, they automatically redirect
resource lookups to a global message source if a local resource cannot
be found.
Currently, the ResourceSetMessageSource
is the
only message source implementation that ships with Spring.NET.
Although automatic localization as described above works well for many form-like pages, it doesn't work nearly as well for controls defined within any iterative controls, because the IDs for such iterative controls are not fixed. Nor does automatic localization work well if you need to display the same resource multiple times within the same page. For example, think of the header columns for outgoing and return flights tables within the SpringAir application (see Chapter 41, SpringAir - Reference Application).
These situations call for a pull-style mechanism for localization,
which is a simple GetMessage
call:
<asp:Repeater id="outboundFlightList" Runat="server"> <HeaderTemplate> <table border="0" width="90%" cellpadding="0" cellspacing="0" align="center" class="suggestedTable"> <thead> <tr class="suggestedTableCaption"> <th colspan="6"> <%= GetMessage("outboundFlights") %> </th> </tr> <tr class="suggestedTableColnames"> <th><%= GetMessage("flightNumber") %></th> <th><%= GetMessage("departureDate") %></th> <th><%= GetMessage("departureAirport") %></th> <th><%= GetMessage("destinationAirport") %></th> <th><%= GetMessage("aircraft") %></th> <th><%= GetMessage("seatPlan") %></th> </tr> </thead> <tbody> </HeaderTemplate>
The GetMessage
method is available within both
the Spring.Web.UI.Page
and
Spring.Web.UI.UserControl
classes, and it falls back
automatically to a global message source lookup if a local resource is
not found.
Unlike text resources, which can be stored within embedded resource files, XML files, or even a database, images in a typical web application are usually stored as files on the file system. Using a combination of directory naming conventions and a custom ASP.NET control, Spring.Web allows you to localize images within the page as easily as you do text resources.
The Spring.Web Page
class exposes the
ImagesRoot
property, with which you define the root
directory where images are stored. The default value is Images, which
means that the localizer expects to find an Images directory within the
application root. But you can set the property to any value in the
definition of the page.
To localize images, you create a directory for each localized
culture under the ImagesRoot
directory:
/MyApp /Images /en /en-US /fr /fr-CA /sr-SP-Cyrl /sr-SP-Latn ...
Once an appropriate folder hierarchy is in place, you put the
localized images in the appropriate directories and make sure that
different translations of the same image have the same image name within
the folders. To place a localized image on a page, you use the
<spring:LocalizedImage>
:
<%@ Page language="c#" Codebehind="StandardTemplate.aspx.cs" AutoEventWireup="false" Inherits="SpringAir.Web.StandardTemplate" %> <%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <html> <body> <spring:LocalizedImage id="logoImage" imageName="spring-air-logo.jpg" borderWidth="0" runat="server" /> </body> </html>
This control will find the most specific directory that contains
an image with the specified name using standard localization fallback
rules and the user's culture. For example, if the user's culture is
'en-US'
, the localizer will look for the
spring-air-logo.jpg
file in
Images/en-US
, then in Images/en
and finally, if the image file has still not been found, in the root
Images
directory (which for all practical purposes
serves as an invariant culture folder).
In addition to global and local resource management, Spring.Web
supports user culture management by exposing the current
CultureInfo
through the
UserCulture
property on the Page
and UserControl
classes.
The UserCulture
property delegates culture
resolution to an implementation of the
Spring.Globalization.ICultureResolver
interface. You
specify the culture resolver to use by configuring the
CultureResolver
property of the
Page
class in the relevant object definition:
<object name="BasePage" abstract="true"> <property name="CultureResolver"> <object type="Spring.Globalization.Resolvers.CookieCultureResolver, Spring.Web"/> </property> </object>
Several useful implementations of
ICultureResolver
ship as part of Spring.Web, so it is
unlikely that application developers need to implement their own culture
resolver. However, you do need to implement your own culture resolver,
the resulting implementation should be fairly straightforward as you
need to implement only two methods. The following sections discuss each
available implementation of the ICultureResolver
interface.
DefaultWebCultureResolver
, the default
culture resolver implementation, is used if you do not specify a
culture resolver for a page, or if you inject a
DefaultWebCultureResolver
into a page definition
explicitly. The latter case (explicit injection) is sometimes useful
because you can specify a culture that should always be used, by
defining the DefaultCulture
property on the
resolver.
The DefaultWebCultureResolver
looks first at
the DefaultCulture
property and return its value if
said property value is not null. If it is null, the
DefaultWebCultureResolver
falls back to request
header inspection. If no 'Accept-Lang'
request
headers are present , the resolver returns the UI culture of the
currently executing thread.
The RequestCultureResolver resolver operates similar to the
DefaultWebCultureResolver
, except that it always
checks request headers first, and only then falls
back to the value of the DefaultCulture
property or
the culture code of the current thread.
The SessionCultureResolver
resolver
looks for culture information in the user's session and returns the
information if it finds it. If not,
SessionCultureResolver
falls back to the
behavior of the DefaultWebCultureResolver
.
This resolver looks for culture information in a cookie, and
return it if it finds one. If not, it falls back to the behavior of
the DefaultWebCultureResolver
.
Warning | |
---|---|
To work around this limitation, use
|
To change the culture, application developers need to define one
of the culture resolvers that support culture changes, such as
SessionCultureResolver
or
CookieCultureResolver
in the Spring application
context. For example,
You also can write a custom ICultureResolver
that persists culture information in a database, as part of a user's
profile.
<object id="cultureResolver" type="Spring.Globalization.Resolvers.SessionCultureResolver, Spring.Web" />
Once that requirement is satisfied, you set the
UserCulture
property to a new
CultureInfo
object before the page is rendered. In
the following .aspx
example, two link buttons can be
used to change the user's culture. In the code-behind, this is all one
need do to set the new culture. A code snippet for the code-behind file
(UserRegistration.aspx.cs
) is shown below.
protected override void OnInit(EventArgs e) { InitializeComponent(); this.english.Command += new CommandEventHandler(this.SetLanguage); this.serbian.Command += new CommandEventHandler(this.SetLanguage); base.OnInit(e); } private void SetLanguage(object sender, CommandEventArgs e) { this.UserCulture = new CultureInfo((string) e.CommandArgument); }
In many ASP.NET applications, no built-in way exists to externalize
the flow of the application. The most common way of defining application
flow is by hardcoding calls to the Response.Redirect
and Server.Transfer
methods within event
handlers.
This approach is problematic because any changes to the flow of an application necessitates code changes (with the attendant recompilation, testing, redeployment, and so on). A better way, which works in many MVC ( Model-View-Controller) web frameworks, is to enable you to externalize the mapping of action results to target pages.
Spring.Web adds this functionality to ASP.NET by allowing you to define result mappings within the definition of a page, and to then simply use logical result names within event handlers to control application flow.
In Spring.Web, a logical result is encapsulated and defined by the
Result
class; thus you can configure results like any
other object:
<objects xmlns="http://www.springframework.net"> <object id="homePageResult" type="Spring.Web.Support.Result, Spring.Web"> <property name="TargetPage" value="~/Default.aspx"/> <property name="Mode" value="Transfer"/> <property name="Parameters"> <dictionary> <entry key="literal" value="My Text"/> <entry key="name" value="%{UserInfo.FullName}"/> <entry key="host" value="%{Request.UserHostName}"/> </dictionary> </property> </object> <object id="loginPageResult" type="Spring.Web.Support.Result, Spring.Web"> <property name="TargetPage" value="Login.aspx"/> <property name="Mode" value="Redirect"/> </object> <object type="UserRegistration.aspx" parent="basePage"> <property name="UserManager" ref="userManager"/> <property name="Results"> <dictionary> <entry key="userSaved" value-ref="homePageResult"/> <entry key="cancel" value-ref="loginPageResult"/> </dictionary> </property> </object> </objects>
The only property for which you must supply a
value for each result is the TargetPage
property. The
value of the Mode
property can be
Transfer
, TransferNoPreserve
,
or Redirect
, and defaults to
Transfer
if none is specified.
TransferNoPreserve issues a server-side transfer with
'preserveForm=false', so that QueryString and Form data are not
preserved.
If your target page requires parameters, you can define them with
the Parameters
dictionary property. You specify literal
values or object navigation expressions
for such parameter values. An expression is evaluated in the context of
the page in which the result is being referenced. In the preceding
example, any page that uses the homePageResult
needs to
expose a UserInfo
property on the page class
itself.
Note | |
---|---|
In Spring.NET 1.1.0 and earlier, the prefix indicated an object
navigation expression in the |
Parameters are handled differently depending on the result mode. For
redirect results, every parameter is converted to a string, then URL
encoded, and finally appended to a redirect query string. Parameters for
transfer results are added to the HttpContext.Items
collection before the request is transferred to the target page. Transfers
are more flexible because any object can be passed as a parameter between
pages. They are also more efficient because they don't require a
round-trip to the client and back to the server, so transfer mode is
recommended as the preferred result mode (it is also the current
default).
Tip | |
---|---|
If you need to customize how a redirect request is generated, for
example, to encrypt the request parameters, subclass the Request object
and override one or more protected methods, for example |
The preceding example shows independent result object definitions,
which are useful for global results such as a home- and login- page.
Result
definitions are only used by one page should be
simply embedded within the definition of a page, either as inner object
definitions or using a special shortcut notation for defining a result
definition:
<object type="~/UI/Forms/UserRegistration.aspx" parent="basePage"> <property name="UserManager"> <ref object="userManager"/> </property> <property name="Results"> <dictionary> <entry key="userSaved" value="redirect:UserRegistered.aspx?status=Registration Successful,user=${UserInfo}"/> <entry key="cancel" value-ref="homePageResult"/> </dictionary> </property> </object>
The short notation for the result must adhere to the following format...
[<mode>:]<targetPage>[?param1,param2,...,paramN]
Possible values for the mode
value referred to in
the preceding notation snippet:
redirect
: calls
Response.Redirect(string)
redirectNoAbort
: calls
Response.Redirect(string, false)
transfer
: calls
Server.Transfer(string)
TransferNoPreserve
: calls
Server.Transfer(string, false)
These values correspond to the values of the ResultMode enumeration. A comma separates parameters instead of an ampersand; this avoids laborious ampersand escaping within an XML object definition. The use of the ampersand character is still supported if required, but you then have to specify the ampersand character using the well known & entity reference.
After you define your results, you can use them within the event
handlers of your pages
(UserRegistration.apsx.cs
):
private void SaveUser(object sender, EventArgs e) { UserManager.SaveUser(UserInfo); SetResult("userSaved"); } public void Cancel(object sender, EventArgs e) { SetResult("cancel"); } protected override void OnInit(EventArgs e) { InitializeComponent(); this.saveButton.Click += new EventHandler(this.SaveUser); this.cancelButton.Click += new EventHandler(this.Cancel); base.OnInit(e); }
You can further refactor the preceding example and use defined constants, which is advisable when a logical result name such as "home" is likely to be referenced by many pages.
You can also register a custom interpreter that can parse the shorthand string representation that creates a Result object. To do this you should view the result mapping string representation as consisting of two parts:
<resultmode>:<textual result representation>
The interface IResultFactory
is responsible for
creating an IResult object from these two pieces:
public interface IResultFactory { IResult CreateResult( string resultMode, string resultText ); }
You use a ResultFactoryRegistry
to associate a
given resultmode string with an IResultFactory
implementation:
class MySpecialResultLogic : IResult { ... } class MySpecialResultLogicFactory : IResultFactory { IResult Create( string mode, string expression ) { /* ... convert 'expression' into MySpecialResultLogic */ } } // register with global factory ResultFactoryRegistry.RegisterResultFactory( "mySpecialMode", new MySpecialResultLogicFactory );
You then use the custom continue mode in your page:
<-- configure your Results --> <object type="mypage.aspx"> <property name="Results"> <dictionary> <entry key="continue" value="mySpecialMode:<some MySpecialResultLogic string representation>" /> </dictionary> </property> </object>
The result redirection is done as before, by calling
myPage.SetResult("cancel");
ASP.NET supports client-side scripting through the use of the
Page.RegisterClientScriptBlock
and
Page.RegisterStartupScript
methods. However, neither
method allows you to output a registered script markup within a
<head>
section of a page, which in many cases is
exactly what you need to do.
Spring.Web adds several methods to enhance client-side scripting
to the base Spring.Web.UI.Page
class:
RegisterHeadScriptBlock
and
RegisterHeadScriptFile
, each with a few overrides.
You can call these methods from your custom pages and controls in order
to register script blocks and script files that must be included in the
<head>
section of the final HTML page.
You must use the <spring:Head>
server-side control to define your <head>
section instead of using the standard HTML
<head>
element. This is shown below.
<%@ Page language="c#" Codebehind="StandardTemplate.aspx.cs" AutoEventWireup="false" Inherits="SpringAir.Web.StandardTemplate" %> <%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <html> <spring:Head runat="server" id="Head1"> <title> <spring:ContentPlaceHolder id="title" runat="server"> <%= GetMessage("default.title") %> </spring:ContentPlaceHolder> </title> <LINK href="<%= CssRoot %>/default.css" type="text/css" rel="stylesheet"> <spring:ContentPlaceHolder id="head" runat="server"></spring:ContentPlaceHolder> </spring:Head> <body> ... </body> </html>
The preceding example above shows how you typically set-up a
<head>
section within a master page template to
be able to change the title value and to add additional elements to the
<head>
section from the child pages using
<spring:ContentPlaceholder>
controls. However,
only the <spring:Head>
declaration is required
in order for Spring.NET Register*
scripts to work
properly.
In a similar fashion, you can add references to CSS files, or even
specific styles, directly to the <head>
HTML
section using Page.RegisterStyle
and
Page.RegisterStyleFile
methods. The latter one simply
allows you to include a reference to an external CSS file, while the
former one allows you to define embedded style definitions by specifying
the style name and definition as the parameters. The final list of style
definitions registered this way will be rendered within the single
embedded style
section of the final HTML
document.
To make the manual inclusion of client-side scripts, CSS files and
images easier, the Spring.Web Page
class exposes
several properties that help you reference such artifacts with absolute
paths. This capability gives web application developers convenience
functionality straight out of the box if they stick to common
conventions such as a web application (directory) structure.
These properties are ScriptsRoot
,
CssRoot
and ImagesRoot
. They have
default values of Scripts
, CSS
and
Images
, which work well if you create and use these
directories in your web application root. However, if you prefer to
place them somewhere else, you can always override default values by
injecting new values into your page definitions (you will typically
inject these values only in the base page definition, as they are
normally shared by all the pages in the application). An example of such
configuration is shown below:
<object name="basePage" abstract="true"> <description> Convenience base page definition for all the pages. Pages that reference this definition as their parent (see the examples below) will automatically inherit the following properties.... </description> <property name="CssRoot" value="Web/CSS"/> <property name="ImagesRoot" value="Web/Images"/> </object>
Spring provides several custom user controls that are located in the
Spring.Web.UI.Controls
namespace. This section lists
the controls and points to other documentation to provide additional
information. Check the SDK docs for descriptions of controls that are not
mentioned here.
You can specify the location in the web page where validation
errors are to be rendered by using the
ValidationSummary
and
ValidationError
controls. Two controls exist because
they have different defaults for how errors are rendered.
ValidationSummary
is used to display potentially
multiple errors identified by the validation framework.
ValidationError
is used to display field-level
validation errors. Please refer to the section ASP.NET usage tips in the
chapter on the Validation Framework
more information.
Some standard controls are not easy to use with Spring's
databinding support. Examples are check boxes and ratio button groups.
Here you should use the CheckBoxList
and
RadioButtonGroup
controls. You can do databinding
itself by using the DataBindingPanel instead of the
using the BindingManager API within the code behind page.
A pop-up DHTML calendar control is provided. It is a slightly modified version of the Dynarch.com DHTML Calendar control written by Mihai Bazon.
You can suppress dependency injection for controls inside your ASP.NET by using the Panel control. See Customizing control dependency injection .