一个Asp.net MVC 控件项目分析---Telerik.Web.Mvc

简介:
  在写本文之前,本人一直抱着‘不宜’在asp.net MVC框架下搞什么控件开发的想法,因为一提到控件就会让人想起‘事件’,‘VIEWSTATE’等一些问题,而asp.net MVC下是Controller, Action, Viewpage, Filter等特性的‘天下’。所以总感觉‘驴唇对不上马嘴’。
      但直到前阵子在邮箱中收到了 关于telerik关于MVC框架扩展的一些信息之后,才发现这家商业控件公司也开始打MVC的主意了。而这个项目(开源)就是该公司在理解了asp.net mvc的基础上所做的一些尝试,当然其所实现的所谓控件与之前我们在项目中所开发或使用的web服务器控件有很大的不同,可以说是抛弃了以往的设计方式。尽管目前它的这种做法我心里还打着问号,但必定是一种尝试(不管你赞同还是不赞同)。下面就做一个简单的分析,希望能给研究MVC架构的朋友提供一些的思考。
      首先要声明的是该开源项目中所使用的js就是jquery,而那些显示效果也基本上就是基于jquery中的那件插件为原型,并进行相应的属性封装,以便于在viewpage中用c#等语言进行声明绑定。下面就其中一些控件的显示截图:
       telerik_mvc_accordion
 
      telerik_mvc_date
 
      telerik_mvc_slider
       telerik_mvc_progressbar
 
       在该开源项目中,所有控件均基于jQueryViewComponentBase (abstract 类型),但其自身属性并不多,而所有的控件基类属性都被jQueryViewComponentBase 的父类 ViewComponentBase所定义,下面以控件中的“Accordion(属性页控件)”为例进行说明,见下图:
telerik_mvc_baseclass
 
      上图中左侧的就是ViewComponentBase类,其定义了多数控件属性,比如js脚本名称和路径以及相关样式以及最终的html元素输出方法,因为其类也是抽象类,所以其中大部分方法均为定义,而未进行具体实现。我们只要关注一下其构造方法就可以了:

   ///   <summary>
    
///  View component base class.
    
///   </summary>
     public   abstract   class  ViewComponentBase : IStyleableComponent, IScriptableComponent
    {
        
private   string  name;

        
private   string  styleSheetFilesLocation;
        
private   string  scriptFilesLocation;

        
///   <summary>
        
///  初始化相关Initializes a new instance of the  <see cref="ViewComponentBase"/>  class.
        
///   </summary>
        
///   <param name="viewContext"> 当前视图的上下文,将会在子类中使用 </param>
        
///   <param name="clientSideObjectWriterFactory"> 传入当前所使用的Writer工厂实例.通过子类注入,子类最终延伸到相对应的控件实例 </param>
         protected  ViewComponentBase(ViewContext viewContext, IClientSideObjectWriterFactory clientSideObjectWriterFactory)
        {
            Guard.IsNotNull(viewContext, 
" viewContext " );
            Guard.IsNotNull(clientSideObjectWriterFactory, 
" clientSideObjectWriterFactory " );

            ViewContext 
=  viewContext;
            ClientSideObjectWriterFactory 
=  clientSideObjectWriterFactory;

            StyleSheetFilesPath 
=  WebAssetDefaultSettings.StyleSheetFilesPath;
            StyleSheetFileNames 
=   new  List < string > ();
            ScriptFilesPath 
=  WebAssetDefaultSettings.ScriptFilesPath;
            ScriptFileNames 
=   new  List < string > ();

            HtmlAttributes 
=   new  RouteValueDictionary();
        } 
    通过上述的构造方法,就可以将控件的一些通用默认属性值进行初始化了。
    下面以“Accordion”的源码来分析一下,这里还是从构造方法入手: 

public   class  Accordion : jQueryViewComponentBase, IAccordionItemContainer
   {
       ……

       
///   <summary>
       
///  Initializes a new instance of the  <see cref="Accordion"/>  class.
       
///   </summary>
       
///   <param name="viewContext"> The view context. </param>
       
///   <param name="clientSideObjectWriterFactory"> The client side object writer factory. </param>
        public  Accordion(ViewContext viewContext, IClientSideObjectWriterFactory clientSideObjectWriterFactory) :  base (viewContext, clientSideObjectWriterFactory)
       {
           Items 
=   new  List < AccordionItem > ();
           autoHeight 
=   true ;
       } 
 
      注:上面的构程方法后面加入了base(viewContext, clientSideObjectWriterFactory),以实现向基类构造方法传参,也就是实现了上面所说的将当前控件所使用的viewContext,clientSideObjectWriterFactory传递到基类ViewComponentBase 中去。(注:最终的clientSideObjectWriterFactory为ClientSideObjectWriterFactory实例类型)。
      当然,因为该控件的中相应属性比较简单,只是一些set,get语法,所以就不过多介绍了,相信做过控件开发的对这些再熟悉不过了。
      下面主要介绍一下其write html元素时所使用的方法,如下:
     
  ///   <summary>
      
///  创建并写入初始化脚本对象和相应属性.
      
///   </summary>
      
///   <param name="writer"> The writer. </param>
       public   override   void  WriteInitializationScript(TextWriter writer)
      {
          
int  selectedIndex  =  Items.IndexOf(GetSelectedItem());

          IClientSideObjectWriter objectWriter 
=  ClientSideObjectWriterFactory.Create(Id,  " accordion " , writer);

          objectWriter.Start()
                      .Append(
" active " , selectedIndex,  0 )
                      .Append(
" animated " , AnimationName)
                      .Append(
" autoHeight " , AutoHeight,  true )
                      .Append(
" clearStyle " , ClearStyle,  false )
                      .Append(
" collapsible " , CollapsibleContent,  false )
                      .Append(
" event " , OpenOn)
                      .Append(
" fillSpace " , FillSpace,  false );

          
if  ( ! string .IsNullOrEmpty(Icon)  ||   ! string .IsNullOrEmpty(SelectedIcon))
          {
              
if  ( ! string .IsNullOrEmpty(Icon)  &&   ! string .IsNullOrEmpty(SelectedIcon))
              {
                  objectWriter.Append(
" icons:{'header':' "   +  Icon  +   " ','headerSelected':' "   +  SelectedIcon  +   " '} " );
              }
              
else   if  ( ! string .IsNullOrEmpty(Icon))
              {
                  objectWriter.Append(
" icons:{'header':' "   +  Icon  +   " '} " );
              }
              
else   if  ( ! string .IsNullOrEmpty(SelectedIcon))
              {
                  objectWriter.Append(
" icons:{'headerSelected':' "   +  SelectedIcon  +   " '} " );
              }
          }

          objectWriter.Append(
" change " , OnChange).Complete();

          
base .WriteInitializationScript(writer);
      } 
     
      可以看出,objectWriter (IClientSideObjectWriter 类型实例)中被绑定了相关的控件属性,并通过其类的WriteInitializationScript(writer)进行脚本的输出。而基本类的相应方法如下:    
    ///   <summary>
      
///  Writes the initialization script.
      
///   </summary>
      
///   <param name="writer"> The writer. </param>
       public   virtual   void  WriteInitializationScript(TextWriter writer)
      {
      } 
 
   
      大家看到该方法为空,但其又是如何运行起来的呢,这里先卖个关子,稍后再说。接着再看一下另一个方法:WriteHtml()
    
   ///   <summary>
      
///  输出当前的 HTML代码.
      
///   </summary>
       protected   override   void  WriteHtml()
      {
          AccordionItem selectedItem 
=  GetSelectedItem();
          TextWriter writer 
=  ViewContext.HttpContext.Response.Output;

          
if  ( ! string .IsNullOrEmpty(Theme))
          {
              writer.Write(
" <div class=\ " { 0 }\ " > " .FormatWith(Theme));
          }

          HtmlAttributes.Merge(
" id " , Id,  false );
          HtmlAttributes.AppendInValue(
" class " "   " " ui-accordion ui-widget ui-helper-reset " );
          writer.Write(
" <div{0}> " .FormatWith(HtmlAttributes.ToAttributeString()));

          
foreach  (AccordionItem item  in  Items)
          {
              item.HtmlAttributes.AppendInValue(
" class " "   " " ui-accordion-header ui-helper-reset ui-state-default  " );
              item.ContentHtmlAttributes.AppendInValue(
" class " "   " " ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom " );

              
if  (item  ==  selectedItem)
              {
                  item.ContentHtmlAttributes.AppendInValue(
" class " "   " " ui-accordion-content-active " );
              }
              
else
              {
                  item.HtmlAttributes.AppendInValue(
" class " "   " " ui-corner-all " );
              }

              writer.Write(
" <h3{0}><a href=\ " #\ " >{1}</a></h3> " .FormatWith(item.HtmlAttributes.ToAttributeString(), item.Text));

              item.ContentHtmlAttributes.AppendInValue(
" style " " ; " , (item  ==  selectedItem)  ?   " display:block "  :  " display:none " );

              writer.Write(
" <div{0}> " .FormatWith(item.ContentHtmlAttributes.ToAttributeString()));
              item.Content();
              writer.Write(
" </div> " );
          }

          writer.Write(
" </div> " );

          
if  ( ! string .IsNullOrEmpty(Theme))
          {
              writer.Write(
" </div> " );
          }

          
base .WriteHtml();
      } 
      
      该方法首先获取当前所选属性页标签(GetSelectedItem()方法),然后用foreach方法对属性页标签集合进行遍历,并判断当前属性页是否就是被选中的属性页,并绑定上相应的css属性。其最终也是调用相应的基类方法进行输出。当然这里基类方法也是为空,呵呵。
 
     准备好了这个控件类之后,Telerik还为Accordion控件‘准备’了一些辅助组件,比如属性页组件(AccordionItem),以及相关的组件构造器(AccordionItemBuilder,AccordionBuilder),这样我们就可以通过这些构造器很方便的创建相应的控件和组件了,下面就以AccordionItemBuilder为例,解释一下其构造器结构:

ContractedBlock.gif Code
     对于上面的OnChange方法,可以使用下面的方法将相应的js脚本传入并执行
.OnChange(()  =>
                                    {
%>
                                        function(
event , ui)
                                        {
                                            $(
' #trace ' ).append( ' Change fired:  '   +   new  Date()  +   ' <br/> ' );
                                        }
                                    
<% }
                              ) 
 
    这样,当属性页发生变化时,就会在页面的指定区域将变化时间显示出来了,如下图:
    telerik_mvc_onchange
  
    Telerik在jQueryViewComponentFactory中对项目中每一个控件提供了一个方法用以初始化相应的构造器,以便于创建相应的控件,比如Accordion,形如:  
    ///   <summary>
     
///  Creates a accordion for ASP.NET MVC view.
     
///   </summary>
     
///   <returns></returns>
     [DebuggerStepThrough]
     
public   virtual  AccordionBuilder Accordion()
     {
         
return   new  AccordionBuilder(Create(()  =>   new  Accordion(ViewContext, clientSideObjectWriterFactory)));
     } 
 
 
    而对于其在VIEW中的使用,则通过扩展方法来加以声明:
   
public   static   class  HtmlHelperExtension
   {
       
private   static   readonly  IClientSideObjectWriterFactory factory  =   new  ClientSideObjectWriterFactory();

       
///   <summary>
       
///  Gets the jQuery view components instance.
       
///   </summary>
       
///   <param name="helper"> The html helper. </param>
       
///   <returns> jQueryViewComponentFactory </returns>
       [DebuggerStepThrough]
       
public   static  jQueryViewComponentFactory jQuery( this  HtmlHelper helper)
       {
           
return   new  jQueryViewComponentFactory(helper, factory);
       }
   } 
    这样在页面视图中,我们这可以使用下面的写法来构造一个Accordion控件了:
<%  Html.jQuery().Accordion()
                  .Name(
" myAccordion " )
                  .Animate(
" bounceslide " )
                  .Items(parent 
=>

  …… 
     上面只是介绍了前台和底层代码如果显示的问题,但还没有解释之前所说的WriteInitializationScript(TextWriter writer)方法以及WriteHtml()
方法如何被调用的问题,正如之前所看到的,因为Accordion的基类ViewComponentBase中未实现具体的代码,所以这里我们要将注意力转移到 jQueryViewComponentFactory中,请看如下代码: 

private  TViewComponent Create < TViewComponent > (Func < TViewComponent >  factory)  where  TViewComponent : ViewComponentBase
       {
           TViewComponent component 
=  factory();

           
if  (component  is  jQueryViewComponentBase)
           {
               component.AssetKey 
=  DefaultAssetKey;
           }

           htmlHelper.Telerik().StyleSheetRegistrar().ToRegistrar().Register(component);
           htmlHelper.Telerik().ScriptRegistrar().ToRegistrar().Register(component);

           
return  component;
       } 
     上面的方法其实就是之前在该类方法Accordion()中所调用并执行的:
      return   new  AccordionBuilder(Create(()  =>   new  Accordion(ViewContext, clientSideObjectWriterFactory)));

     通过该方法,就可以将该控件及其相关组件信息注册到相应的视图中。因为我们比较关注WriteHtml()方法,所以这里就直接分析一下这一行代码:    
     ScriptRegistrar().ToRegistrar().Register(component); 
 
     ScriptRegistrar类中的Register方法承担着将当前要创建的组件添加到当前的 脚本组件列表中的任务(scriptableComponents为list列表)     
        ///   <summary>
       
///  Registers the scriptable component.
       
///   </summary>
       
///   <param name="component"> The component. </param>
        public   virtual   void  Register(IScriptableComponent component)
       {
           Guard.IsNotNull(component, 
" component " );

           
if  ( ! scriptableComponents.Contains(component))
           {
               scriptableComponents.Add(component);
           }
       } 
    
      当组件被成功添加到该list列表中后,系统就会调用Render()方法将其显示出来(注:该方法与以前web控件开发中的显示方法同名,所以比较好理解),如下:    
       ///   <summary>
      
///  Writes the scripts in the response.
      
///   </summary>
       public   void  Render()
      {
          
if  (hasRendered)
          {
              
throw   new  InvalidOperationException(Resources.TextResource.YouCannotCallRenderMoreThanOnce);
          }

          
if  (ViewContext.HttpContext.Request.Browser.EcmaScriptVersion.Major  >=   1 )
          {
              Write(ViewContext.HttpContext.Response.Output);
          }

          hasRendered 
=   true ;
      } 
 
     注意上面的这一行代码:
   Write(ViewContext.HttpContext.Response.Output);
 
      其所实现的功能如下:   
       ///   <summary>
      
///  写出所有脚本资源和脚本 statements.
      
///   </summary>
      
///   <param name="writer"> The writer. </param>
       protected   virtual   void  Write(TextWriter writer)
      {
          WriteScriptSources(writer);
          WriteScriptStatements(writer);
      } 

       而就是WriteScriptStatements这行代码开始执行之前所说的那个WriteInitializationScript(TextWriter writer)。而WriteHtml()方法的执行入口要更加复杂一些,因为Telerik提供了ViewComponentBuilderBase这个类来进行视图组件的构造,而该类中的Render方法就是对相应组件的Render方法(组件中已定义)进行调用,如下:
     ///   <summary>
    
///  Renders the component.
    
///   </summary>
     public   virtual   void  Render()
    {
        Component.Render();
    } 
 
      而之前的“Accordion”控件是继承自ViewComponentBase类,所以相应组件的Render方法就在该类中进行了声明定义,如下:
      ///   <summary>
     
///  Renders the component.
     
///   </summary>
      public   void  Render()
     {
         EnsureRequired();
         WriteHtml();
     } 
 
      大家看到了第二行代码了吧,这就是我们之前看到的那个方法,也就是Accordion组件中WriteHtml()重写方法的调用入口。
 
      绕了这么一大圈,才把这个流程理清,是不是有些晕了。的确,刚开始接触时我也有点晕,但晕呀晕呀就‘晕过去了’,现在再回头看起来感常见其整体的架构思路还是很清晰的。可以说有了这瓶酒垫底,再分析该项目中的其它控件就‘如鱼得水’了。
 
       最后不妨总结一下:
      该项目中对asp.net mvc控件下的开发做了一次尝试,但如果之前做过控件特别是web服务器端控件开发的朋友,可以看出项目中有非常重的web控件开发味道,基本连方法名称都有一定的重叠。
      另外就是其自身还是引用了组件对象模型的概念,就拿属性页控件来说,就将其分为Accordion和AccordionItem两种类型,其中可以将Accordion看成是AccordionItem的集合封装(包括遍历操作),而这里AccordionItem就成了Accordion的一个组件,而Accordion又是当前view中的一个组件。而组件开发一直是.net平台上所倡导的。其优势在于可复用,维护方便,简化复杂问题等。


本文转自 daizhenjun 51CTO博客,原文链接:http://blog.51cto.com/daizhj/216764,如需转载请自行联系原作者
相关文章
|
1月前
|
开发框架 JavaScript 前端开发
震撼!破解 ASP.NET 服务器控件 Button 执行顺序之谜,颠覆你的开发认知!
【8月更文挑战第16天】在ASP.NET开发中,通过Button控件实现先执行JavaScript再触后台处理的需求十分常见。例如,在用户点击按钮前需前端验证或提示,确保操作无误后再传递数据至后台深度处理。此过程可通过设置Button的`OnClientClick`属性调用自定义JavaScript函数完成验证;若验证通过,则继续触发后台事件。此外,结合jQuery也能达到相同效果,利用`__doPostBack`手动触发服务器端事件。这种方式增强了应用的交互性和用户体验。
37 8
|
14天前
|
存储 运维
.NET开发必备技巧:使用Visual Studio分析.NET Dump,快速查找程序内存泄漏问题!
.NET开发必备技巧:使用Visual Studio分析.NET Dump,快速查找程序内存泄漏问题!
|
19天前
|
前端开发 安全 Java
技术进阶:使用Spring MVC构建适应未来的响应式Web应用
【9月更文挑战第2天】随着移动设备的普及,响应式设计至关重要。Spring MVC作为强大的Java Web框架,助力开发者创建适应多屏的应用。本文推荐使用Thymeleaf整合视图,通过简洁的HTML代码提高前端灵活性;采用`@ResponseBody`与`Callable`实现异步处理,优化应用响应速度;运用`@ControllerAdvice`统一异常管理,保持代码整洁;借助Jackson简化JSON处理;利用Spring Security增强安全性;并强调测试的重要性。遵循这些实践,将大幅提升开发效率和应用质量。
46 7
|
19天前
|
开发框架 JavaScript 前端开发
|
17天前
|
前端开发 测试技术 开发者
MVC模式在现代Web开发中有哪些优势和局限性?
MVC模式在现代Web开发中有哪些优势和局限性?
|
1月前
|
XML 开发框架 .NET
ASP.NET Web Api 如何使用 Swagger 管理 API
ASP.NET Web Api 如何使用 Swagger 管理 API
|
21天前
|
开发者 前端开发 Java
架构模式的诗与远方:如何在MVC的田野上,用Struts 2编织Web开发的新篇章
【8月更文挑战第31天】架构模式是软件开发的核心概念,MVC(Model-View-Controller)通过清晰的分层和职责分离,成为广泛采用的模式。随着业务需求的复杂化,Struts 2框架应运而生,继承MVC优点并引入更多功能。本文探讨从MVC到Struts 2的演进,强调架构模式的重要性。MVC将应用程序分为模型、视图和控制器三部分,提高模块化和可维护性。
32 0
|
21天前
|
Java 开发者 前端开发
Struts 2、Spring MVC、Play Framework 上演巅峰之战,Web 开发的未来何去何从?
【8月更文挑战第31天】在Web应用开发中,Struts 2框架因强大功能和灵活配置备受青睐,但开发者常遇配置错误、类型转换失败、标签属性设置不当及异常处理等问题。本文通过实例解析常见难题与解决方案,如配置文件中遗漏`result`元素致页面跳转失败、日期格式不匹配需自定义转换器、`&lt;s:checkbox&gt;`标签缺少`label`属性致显示不全及Action中未捕获异常影响用户体验等,助您有效应对挑战。
44 0
|
21天前
|
Java 前端开发 Apache
Apache Wicket与Spring MVC等Java Web框架大PK,究竟谁才是你的最佳拍档?点击揭秘!
【8月更文挑战第31天】在Java Web开发领域,众多框架各具特色。Apache Wicket以组件化开发和易用性脱颖而出,提高了代码的可维护性和可读性。相比之下,Spring MVC拥有强大的生态系统,但学习曲线较陡;JSF与Java EE紧密集成,但在性能和灵活性上略逊一筹;Struts2虽成熟,但在RESTful API支持上不足。选择框架时还需考虑社区支持和文档完善程度。希望本文能帮助开发者找到最适合自己的框架。
27 0
|
21天前
|
存储 前端开发 数据库
神秘编程世界惊现强大架构!Web2py 的 MVC 究竟隐藏着怎样的神奇魔力?带你探索实际应用之谜!
【8月更文挑战第31天】在现代 Web 开发中,MVC(Model-View-Controller)架构被广泛应用,将应用程序分为模型、视图和控制器三个部分,有助于提高代码的可维护性、可扩展性和可测试性。Web2py 是一个采用 MVC 架构的 Python Web 框架,其中模型处理数据和业务逻辑,视图负责呈现数据给用户,控制器则协调模型和视图之间的交互。
25 0