将一个ASP.NET站点分离为多个独立的模块,一个最大的问题就是与页面相关联的大多数业务逻辑驻留在该页面的源代码文件中,我们几乎做不到将源代码文件分为多个独立的程序集。为了真正创建独立的与站点中的页面相关联的功能模块,所有页面逻辑、事件处理逻辑和导航逻辑需要以某种方式从页面提取出来并保存在独立的程序集中。
Web Composite应用程序块中的默认解决方案是使用 View-Presenter 模式将页面逻辑分成不同的用于响应由视图(网页)转发的任意事件的类(表示器)。表示器类完全在业务模块中实现,从而将应用程序逻辑至于网站外,接口是在定义由视图实现的方法的业务模块中定义的。这样,网页可在结束时将所有事件转发给表示器,无需任何实际的特定于应用程序的职责。这样还可使设计表示器的测试更为容易,无需实际涉及到前端网页。
还是接着我们在上一篇中的示例,已经创建了相关的业务模块和服务,这里添加一个视图:
这里我们创建一个显示商品详细信息的视图ProductDetail,创建完成后,在资源管理器中可以看到:
添加了新页面ProductDetail.aspx 以及相应的源代码文件添加到 /Products 目录。对于Products 业务模块项目,该方案将添加新的类 ProductDetailPresenter (表示器)和相应的 IProductDetail (视图)接口,该接口已经由 ProductDetail.aspx文件中的源代码类实现,同时源代码类文件还包含一个页面应向其转发事件的相应的 ProductDetailPresenter 类的属性声明:
public partial class Products_ProductDetail : System.Web.UI.Page, IProductDetail { private ProductDetailPresenter _presenter; protected void Page_Load(object sender, EventArgs e) { if (!this.IsPostBack) { this._presenter.OnViewInitialized(); } this._presenter.OnViewLoaded(); } [CreateNew] public ProductDetailPresenter Presenter { set { this._presenter = value; this._presenter.View = this; } } }
由于我们要显示Product的名称和品牌,打开IProductDetail.cs文件,添加如下两个属性:
public interface IProductDetail { public string Name; public string Brand; }
然后我们在页面中实现这两个属性:
public partial class Products_ProductDetail : System.Web.UI.Page, IProductDetail { private ProductDetailPresenter _presenter; protected void Page_Load(object sender, EventArgs e) { if (!this.IsPostBack) { this._presenter.OnViewInitialized(); } this._presenter.OnViewLoaded(); } [CreateNew] public ProductDetailPresenter Presenter { set { this._presenter = value; this._presenter.View = this; } } public string Name { set { this.lbl_Name.Text = value; } } public string Brand { set { this.lbl_Brand.Text = value; } } }
从上面的代码中可以看到,页面的源代码文件中非常干净,没有任何与业务逻辑有关的东西,至于数据从哪儿来,该怎么进行显示,就交给Presenter。接下来就要实现Presenter。打开ProductDetailPresenter.cs文件,这里可以实现页面中的任何事件,其中OnViewLoaded、OnViewInitialized两个方法的区别在于对应我们在Page中的IsPostBack判断,View属性是定义在泛型的Presenter中。编写完成后代码如下:
public class ProductDetailPresenter : Presenter<IProductDetail> { private ProductsController _controller; public ProductDetailPresenter([CreateNew] ProductsController controller) { _controller = controller; } public override void OnViewLoaded() { Product product = _controller.GetProductById("1"); View.Name = product.Name; View.Brand = product.Brand; } public override void OnViewInitialized() { } }
在浏览器中查看后,可以看到页面如下:
这里有个问题是页面的显示出来了,但是在左边的树形控件导航中并没有看到ProcuctDetail的链接。这是下面要说的模块站点映射。
在解决方案母版页面中,包含一个绑定到 SiteMapDataSource 的树视图控件,用于显示站点上的可导航页面,另外还有SiteMapPath控件。这里的SiteMapDataSource并没有绑定到标准 Web.sitemap 文件,而是绑定到独立地从每个模块收集导航信息的自定义SiteMapProvider。该Provider在应用程序启动时显式询问每个模块的站点地图信息,默认情况下,此 SiteMapProvider 作为默认提供程序注册,并将由 SiteMapDataSource 用于所有导航控件。
要使用特定模块的站点地图信息填充 ModuleSiteMapProvider,需要覆写从 ModuleInitializer 派生的类中的 RegisterSiteMapInformation 方法。我们在创建业务模块时会覆写此方法,并会将业务模块下的Default.aspx 页面插入到 SiteMapNodes 集合,但是我们需要将其他任何页面添加到站点的同时也将其添加到集合中。
protected virtual void RegisterSiteMapInformation(ISiteMapBuilderService siteMapBuilderService) { SiteMapNodeInfo moduleNode = new SiteMapNodeInfo("Products", "~/Products/Default.aspx", "Products"); siteMapBuilderService.AddNode(moduleNode); SiteMapNodeInfo productDetailNode = new SiteMapNodeInfo("ProductDetail","~/Products/ProductDetail.aspx","ProductDetail"); siteMapBuilderService.AddNode(productDetailNode,moduleNode); }
SiteMapNodeInfo有三个参数,分别对应为键值、页面地址、显示标题。不过这里又添加了硬编码,可以根据自己的需要修改,使其能够从配置文件中读取站点映射信息。
现在再运行程序,可以看到ProductDetail页面已经添加在了Products业务模块下,并且页面上已经添加了导航栏。
利用Web Client Software Factory,一个简单的Composite Web应用程序块和View-Presenter模式结合的使用就到这里了。下篇我们做更接近实际的例子,如何使用View-Presenter模式进行数据绑定和ObjectContainerDataSource控件的使用。
示例代码下载:/Files/Terrylee/WebClientDemo1.rar