【轉載】直接通過User Control生成HTML

對於使用User Control生成HTML的方式,大家應該已經比較熟悉了,老趙也曾經寫過一篇文章(《技巧:使用User Control做HTML生成》)來描述這個做法。使用User Control進行HTML生成最大的好處就是將表現(Presentation)邏輯集中在一處,並且能夠讓前臺開發人員使用傳統的方式參與到頁面開發中來。在其他方面,使用User Control生成HTML的做法直接使用了ASP.NET WebForms引擎,在開發時能夠利用到ASP.NET的各種優秀實踐。

在“我的衣櫥”中大量使用了這種生成HTML的做法。不過當項目達到一定規模之後,這個方法的不足之處也慢慢地體現了出來。就拿《技巧:使用User Control做HTML生成》作爲例子來講,除了顯示上必要的Comments.aspx頁面和Comments.ascx控件之外,還有一個額外的GetComments.ashx進行客戶端與服務器端之間的通信。不過問題就出在這裏,當此類做法越來越多時,項目中就會出現大量的此類ashx文件。冗餘代碼的增加降低了代碼的可維護性,試想如果我們需要在某個控件上增加一個額外的屬性,就需要去Handler那裏編寫對應的邏輯。雖不算是繁重的工作,但是如果能解決這個問題,無疑是一個錦上添花的做法。

如果要避免大量Handler的出現,必然需要找到這些Handler的共同之處。這一點並不困難,因爲每個Handler的邏輯的確大同小異:

  1. 使用ViewManager加載User Control
  2. 從QueryString或Form中獲取參數,並設置對應屬性
  3. 使用ViewManager生成HTML代碼,並使用Response.Write輸出

寫一個統一的Handler來將User Control轉化爲HTML是一件輕而易舉的事情。不過因爲有第2步,我們就必須應對爲不同控件賦不同值的情況。這種“描述性”的內容即是該控件的“元數據(metadata)”,而說到“元數據”各位應該就能很快想到自定義屬性(Custom Attribute)這個.NET特有的事物。因此我們的解決方案也使用這種方式來對控件的屬性進行“描述”,且看該屬性的定義:

public enum UserControlRenderingPropertySource
{
    Form,
    QueryString
}
 
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class UserControlRenderingPropertyAttribute : Attribute
{
    public string Key { get; set; }
 
    public UserControlRenderingPropertySource Source { get; set; }
}

這個自定義屬性只能標記在屬性上,而它的作用是定義這個屬性值的來源(是Query String還是Form)與集合中的鍵名。而我們的實現還會根據屬性上標記的DefaultValueAttribute爲屬性設定默認值。定義了Custom Attrubte之後,我們就可以編寫這個統一的Handler了。爲了在客戶端“隱藏”直接請求ascx文件的事實,我們只響應對擴展名爲“ucr”的請求:

public class UserControlRenderingHandler : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        string appRelativePath = context.Request.AppRelativeCurrentExecutionFilePath;
        string controlPath = appRelativePath.ToLower().Replace(".ucr", ".ascx");

        var viewManager = new ViewManager<UserControl>();
        var control = viewManager.LoadViewControl(controlPath);
 
        SetPropertyValues(control, context);
        
        context.Response.ContentType = "text/html";
        context.Response.Write(viewManager.RenderView(control));
    }
}

上面代碼中的SetPropertyValues方法便是爲User Control實例的屬性賦值:

private static void SetPropertyValues(UserControl control, HttpContext context)
{
    var metadata = GetMetadata(control.GetType());
    foreach (var property in metadata.Keys)
    {
        object value = GetValue(metadata[property], context) ?? GetDefaultValue(property);
        if (value != null)
        {
            property.SetValue(control, Convert.ChangeType(value, property.PropertyType), null);
        }
    }
}

  SetPropertyValues方法中會調用三個方法:GetMetadata,GetValue和GetDefaultValue。GetMetadata方法會得到關於這個control的元數據,爲PropertyInfo - List<UserControlRenderingPropertyAttribute>的鍵值對(即Dictionary)。然後將metadata傳入GetValue方法,以獲取由UserControlRenderingPropertyAttribute標記的值。如果GetValue方法返回null,則調用GetDefaultValue方法獲取標記在該屬性上DefaultValueAttribute。以下就將其餘代碼附上,沒有技術含量,但做一參考:

[展開代碼]

private static Dictionary<
    Type,
    Dictionary<
        PropertyInfo,
        List<UserControlRenderingPropertyAttribute>>> s_metadataCache =
    new Dictionary<
        Type,
        Dictionary<
            PropertyInfo,
            List<UserControlRenderingPropertyAttribute>>>();
private static Dictionary<PropertyInfo, object> s_defaultValueCache =
    new Dictionary<PropertyInfo,object>();
private static object s_mutex = new object();
 
private static Dictionary<
    PropertyInfo,
    List<UserControlRenderingPropertyAttribute>> GetMetadata(Type type)
{
    if (!s_metadataCache.ContainsKey(type))
    {
        lock (s_mutex)
        {
            if (!s_metadataCache.ContainsKey(type))
            {
                s_metadataCache[type] = LoadMetadata(type);
            }
        }
    }
 
    return s_metadataCache[type];
}
 
private static Dictionary<
    PropertyInfo,
    List<UserControlRenderingPropertyAttribute>> LoadMetadata(Type type)
{
    var result = new Dictionary<PropertyInfo, List<UserControlRenderingPropertyAttribute>>();
    PropertyInfo[] properties = type.GetProperties(
        BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty);
 
    foreach (var p in properties)
    {
        var attributes = p.GetCustomAttributes(
            typeof(UserControlRenderingPropertyAttribute), true);
        if (attributes.Length > 0)
        {
            result[p] = new List<UserControlRenderingPropertyAttribute>(
                attributes.Cast<UserControlRenderingPropertyAttribute>());
        }
    }
 
    return result;
}
 
private static object GetDefaultValue(PropertyInfo property)
{
    if (!s_defaultValueCache.ContainsKey(property))
    {
        lock (s_mutex)
        {
            if (!s_defaultValueCache.ContainsKey(property))
            {
                var attributes = property.GetCustomAttributes(typeof(DefaultValueAttribute), true);
                object value = attributes.Length > 0 ?
                    ((DefaultValueAttribute)attributes[0]).Value : null;
                s_defaultValueCache[property] = value;
            }
        }
    }
 
    return s_defaultValueCache[property];
}
 
private static void SetPropertyValues(UserControl control, HttpContext context)
{
    var metadata = GetMetadata(control.GetType());
    foreach (var property in metadata.Keys)
    {
        object value = GetValue(metadata[property], context) ?? GetDefaultValue(property);
        if (value != null)
        {
            property.SetValue(control, Convert.ChangeType(value, property.PropertyType), null);
        }
    }
}
 
private static object GetValue(
    IEnumerable<UserControlRenderingPropertyAttribute> metadata,
    HttpContext context)
{
    foreach (var att in metadata)
    {
        var collection = (att.Source == UserControlRenderingPropertySource.QueryString) ?
                context.Request.QueryString : context.Request.Params;
        object value = collection[att.Key];
 
        if (value != null) return value;
    }
 
    return null;
}

至此,UserControlRenderingHandler完成了。不過在真正使用時,還需要進行一些配置。例如,您需要在IIS的ISAPI Mapping中將“.ucr”與aspnet_isapi.dll進行映射,並且在web.config中將*.ucr與UserControlRenderingHandler關聯起來。當然,對User Control的屬性進行標記是必須的。例如還是《技巧:使用User Control做HTML生成》一文中的例子:

public partial class Comments : System.Web.UI.UserControl
{
    protected override void OnPreRender(EventArgs e)
    {
        // ...
    }
 
    [UserControlRenderingProperty(Key = "page", Source = UserControlRenderingPropertySource.QueryString)]
    public int PageIndex { get; set; }
 
    [DefaultValue(10)]
    public int PageSize { get; set; }
 
    // ...
}

然後,在客戶端代碼中只要根據路徑發起請求即可,UserControlRenderingHandler會在服務器端完成餘下的工作。

 

 

<script type="text/javascript" language="javascript">
    function getComments(pageIndex)
    {
        new Ajax.Updater(
            "comments",
            "/Controls/Comments.ucr?page=" + pageIndex + "&t=" + new Date(),
            { method: "get" }); 
        
        return false; // IE only
    }
</script>

不過,這就夠了嗎?對於一個例子來說,這已經足夠了,不過要在產品環境中使用很可能還略顯不夠。例如,如果只讓用戶訪問到特定的User Control,或者只有特定權限的用戶才能訪問,又該如何對UserControlRenderingHandler進行改造呢?相信您瞭解上述做法之後,這點要求對您一定不成問題。

 

原載地址:趙劼http://blog.zhaojie.me/2008/07/user-control-rendering.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章