65.9K
CodeProject 正在变化。 阅读更多。
Home

嵌入式用户控件:重访

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (18投票s)

2006年12月18日

6分钟阅读

viewsIcon

197377

downloadIcon

1458

使用 VS2005 SP1 可以轻松地将 UserControls 嵌入到 .NET 程序集中

引言

Visual Studio 2005 Service Pack 1 恢复了对 ASP.NET Web 项目的支持,使其能够将 ASPX 和 ASCX 页面编译到单个程序集中。结合设计时所见即所得的支持和巧妙的资源嵌入用法,可以利用 ASP.NET UserControl 技术创建与一流自定义控件一样灵活和强大的 Web 控件库。

背景

Visual Studio 2005 的初始版本取消了对“Web 项目”的支持,这让 Web 开发人员非常失望。作为回应,出现了一个 Web 应用程序项目选项来恢复此功能,但您必须单独下载并安装它。随着 Service Pack 1 的发布,此功能已恢复,现在我们将对其进行试用!

必备组件

项目设置

为了演示嵌入式控件的功能,我们需要创建两个项目

  • 一个控件库
  • 一个测试网站

您可以使用此项目附带的项目存根,或者如果您愿意,也可以从头开始设置自己的项目。完成后,您的解决方案布局应有一个用于测试的 Web 项目和一个将包含您的嵌入式控件的 Web 项目。

要从头开始,请创建一个新的 ASP.NET Web 应用程序项目。请注意此处区别 - 我们选择“新建”>“项目”。

由于我们正在创建一个程序集,请删除 Default.aspxweb.config。这将使您得到一个空项目。

创建 VirtualPathProvider.cs 或从示例项目中复制它。

using System.Web.Hosting;
using System.Web.Caching;
using System.Collections;
using System;
using System.IO;
using System.Web;
using System.Reflection;

namespace ControlLibrary
{
    public class AssemblyResourceProvider : VirtualPathProvider
    {
        string mResourcePrefix;

        public AssemblyResourceProvider()
            :this("EmbeddedWebResource")
        {
        }

        public AssemblyResourceProvider(string prefix)
        {
            mResourcePrefix = prefix;
        }

        private bool IsAppResourcePath(string virtualPath)
        {
            String checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
            return checkPath.StartsWith("~/"+mResourcePrefix+"/",
                   StringComparison.InvariantCultureIgnoreCase);
        }

        public override bool FileExists(string virtualPath)
        {
            return (IsAppResourcePath(virtualPath) ||
                    base.FileExists(virtualPath));
        }

        public override VirtualFile GetFile(string virtualPath)
        {
            if (IsAppResourcePath(virtualPath))
                return new AssemblyResourceVirtualFile(virtualPath);
            else
                return base.GetFile(virtualPath);
        }

        public override CacheDependency
               GetCacheDependency(string virtualPath,
               IEnumerable virtualPathDependencies,
               DateTime utcStart)
        {
            if (IsAppResourcePath(virtualPath))
                return null;
            else
                return base.GetCacheDependency(virtualPath,
                       virtualPathDependencies, utcStart);
        }
    }

    class AssemblyResourceVirtualFile : VirtualFile
    {
        string path;

        public AssemblyResourceVirtualFile(string virtualPath)
            : base(virtualPath)
        {
            path = VirtualPathUtility.ToAppRelative(virtualPath);
        }

        public override Stream Open()
        {
            string[] parts = path.Split('/');
            string assemblyName = parts[2];
            string resourceName = parts[3];
            assemblyName = Path.Combine(HttpRuntime.BinDirectory,
                                        assemblyName);
            Assembly assembly = Assembly.LoadFile(assemblyName);
            if (assembly == null) throw new Exception("Failed to load " + 
                                assemblyName);
            Stream s = assembly.GetManifestResourceStream(resourceName);
            if (s == null) throw new Exception("Failed to load " + 
                                resourceName);
            return s;
        }
    }
}

关注点

  • 请注意,构造函数接受一个可配置的前缀。这与原始代码不同。
  • 请注意,如果找不到程序集或资源名称,则会抛出异常。这与原始代码不同。

创建或复制 EmbeddedUserControlLoader.cs 类。这是为我们提供 VS2005 工具箱支持的类。

using System.Web.UI.WebControls;
using System.Web.UI;
using System;
using System.ComponentModel;
using System.Web;

namespace ControlLibrary
{
    [ToolboxData("<{0}:EmbeddedUserControlLoader runat=server>
                    </{0}:EmbeddedUserControlLoader>")]
    public class EmbeddedUserControlLoader : WebControl
    {
        # region VirtualPathProvider setup code
        static EmbeddedUserControlLoader()
        {
            if (!IsDesignMode)
            {
                System.Web.Hosting.HostingEnvironment.
                RegisterVirtualPathProvider(
            new AssemblyResourceProvider(ResourcePrefix));
            }
        }

        static bool IsDesignMode
        {
            get
            {
                return HttpContext.Current == null;
            }
        }

        static string mResourcePrefix = "EmbeddedWebResource";

        public static string ResourcePrefix
        {
            get
            {
                return mResourcePrefix;
            }

            set
            {
                mResourcePrefix = value;
            }
        }
        #endregion

        #region Toolbox properties
        private string mAssemblyName = "";

        [Bindable(true)]
        [Category("Behavior")]
        [Localizable(true)]
        public string AssemblyName
        {
            get { return mAssemblyName; }
            set { mAssemblyName = value; }
        }

        private string mControlNamespace = "";

        [Bindable(true)]
        [Category("Behavior")]
        [Localizable(true)]
        public string ControlNamespace
        {
            get { return mControlNamespace; }
            set { mControlNamespace = value; }
        }

        private string mControlClassName = "";

        [Bindable(true)]
        [Category("Behavior")]
        [Localizable(true)]
        public string ControlClassName
        {
            get { return mControlClassName; }
            set { mControlClassName = value; }
        }
        #endregion

        #region Private members

        string Path
        {
            get
            {
                return String.Format("/{0}/{1}.dll/{2}.{3}.ascx", 
            ResourcePrefix, AssemblyName, ControlNamespace, 
            ControlClassName);
            }
        }

        Control c;

        protected override void OnInit(EventArgs e)
        {
            c = Page.LoadControl(Path);
            Controls.Add(c);
            base.OnInit(e);
        }

        protected override void RenderContents(HtmlTextWriter output)
        {
            if (IsDesignMode)
            {
                output.Write(Path);
                return;
            }
            base.RenderContents(output);
        }
        #endregion

        #region Helper members to access UserControl properties

        public void SetControlProperty(string propName, object value)
        {
            c.GetType().GetProperty(propName).SetValue(c, value, null);
        }

        public object GetControlProperty(string propName)
        {
            return c.GetType().GetProperty(propName).GetValue(c, null);
        }
        #endregion
    }
}

关注点

  • 静态方法

    虚拟路径的注册有一个特殊的顺序与之相关。Microsoft 建议“在 Web 应用程序执行任何页面解析或编译之前”注册所有虚拟路径,并且稍后注册它们不会影响已编译和缓存的页面。

    在 Web 门户设置中,您并不总是有权访问 Application_Start()。因此,我更喜欢一种替代方法,即在用户代码中初始化提供程序。在我测试时,在 EmbeddedUserControlLoaderstatic 构造函数中注册没有问题,因为这会在该类的任何实例执行之前执行,因此虚拟路径会及时配置,以便 OnInit 可以使用它。有关更多信息,请参阅 MSDN 上的 VirtualPathProvider

  • 静态前缀属性

    您的控件库必须实现一个唯一的前缀才能使虚拟路径正常工作。此属性允许您在运行时配置前缀。

  • Get/Set 方法

    此类允许您 get set 底层控件的属性。稍后将详细介绍。

  • 工具箱设计时属性

    您可以指定要在设计时加载的程序集名称、命名空间和控件名称。稍后将详细介绍。

最后一步是创建您的测试网站。这可以是新的 ASP.NET Web 项目,也可以是传统的 VS2005 网站项目。保留 default.aspxweb.config,并确保添加对 ControlLibrary 程序集的引用。

创建和嵌入您的第一个 UserControl

有了解决方案存根,我们就可以开始工作了!让我们创建一个“Hello World”UserControl 来进行演示。

在您的 ControlLibrary 项目中创建一个新的 UserControl(命名为 HelloControl)。在 UserControl 中放入一些文本。

然后 - 这是关键步骤 - 将 ASCX 文件的 Build Action 设置为 Embedded Resource。就这样!您已完成。编译。您的 ASCX 文件现在已嵌入 ControlLibrary.dll 中。

现在转到您已包含 ControlLibrary 引用的测试站点。打开 Default.aspx,然后将 EmbeddedUserControlLoader 控件从 VS2005 工具箱拖到您的页面上。您可以查看 ControlLibrary 中的 HelloControl.ascx.cs 来获取 AssemblyNameControlClassNameControlNamespace 的适当值。更改这些值时,控件的设计时表示将显示它打算加载的嵌入式资源的路径。

运行 Default.aspx ,然后惊叹不已。您的用户控件直接从 ControlLibrary 程序集中无缝加载!

既然您已经完成了一些基本操作,那么让我们来看一些更复杂的场景。

高级技术:嵌套控件

您的嵌入式 UserControls 可以嵌套。这工作得非常好,我将留给您自己去尝试。只需将 EmbeddedUserControlLoader 对象拖放到您在 ControlLibrary 项目中创建的 UserControls 中即可。您可以随意嵌套控件。

高级技术:属性访问器

您可以使用 EmbeddedUserControlLoader 来访问 UserControls 的属性。这值得详细理解,因为它是使您的控件感觉像真正的自定义控件的唯一因素。

让我们创建一个反馈控件。我们想收集一些基本信息并通过属性公开。

FeedbackControl.ascx

<%@ Control Language="C#" AutoEventWireup="true" 
                Codebehind="FeedbackControl.ascx.cs"
Inherits="ControlLibrary.FeedbackControl" %>
<h1>
Your Feedback is <span style="font-size: 5pt">not</span> Valuable</h1>
Name:
<asp:TextBox ID="txtName" runat="server"></asp:TextBox><br />
Subject:
<asp:TextBox ID="txtSubject" runat="server"></asp:TextBox><br />
Message:<br />
<asp:TextBox ID="txtMessage" runat="server" Height="256px" 
            TextMode="MultiLine" Width="353px"></asp:TextBox>

FeedbackControl.ascx.cs

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

namespace ControlLibrary
{
    public partial class FeedbackControl : System.Web.UI.UserControl
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        public string Name
        {
            get
            {
                return this.txtName.Text;
            }
        }

        public string Subject
        {
            get
            {
                return this.txtSubject.Text;
            }
        }

        public string Message
        {
            get
            {
                return this.txtMessage.Text;
            }
        }
    }
}    

现在,让我们创建一个反馈表单来收集信息。我们将不发送电子邮件,而是在回发时将其显示给用户。

Feedback.aspx

<%@ Page Language="C#" AutoEventWireup="true" 
    Codebehind="Feedback.aspx.cs" Inherits="DemoSite.Feedback" %>
<%@ Register Assembly="ControlLibrary" Namespace="ControlLibrary" 
                        TagPrefix="cc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <cc1:EmbeddedUserControlLoader ID="EmbeddedUserControlLoader1" 
        runat="server" AssemblyName="ControlLibrary"
                ControlClassName="FeedbackControl" 
        ControlNamespace="ControlLibrary">
        </cc1:EmbeddedUserControlLoader>
            <br />
            <br />
            <asp:Button ID="Button1" runat="server" OnClick="Button1_Click" 
                    Text="Submit Feedback" /><br />
            <br />
            <asp:Panel ID="Panel1" runat="server" Height="244px" 
                    Visible="False" Width="383px">
                Name:
                <asp:Label ID="lblName" runat="server" Text="Label">
                            </asp:Label><br />
                Subject:
                <asp:Label ID="lblSubject" runat="server" Text="Label">
                            </asp:Label><br />
                Message:<br />
                <asp:Label ID="lblMessage" runat="server" Text="Label">
                        </asp:Label></asp:Panel>
            &nbsp;</div>
    </form>
</body>
</html>

Feedback.aspx.cs

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

namespace DemoSite
{
    public partial class Feedback : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
        }

        protected void Button1_Click(object sender, EventArgs e)
        {
            this.Panel1.Visible = true;
            this.lblName.Text = 
        this.EmbeddedUserControlLoader1.GetControlProperty("Name") 
            as string;
            this.lblSubject.Text= 
        this.EmbeddedUserControlLoader1.GetControlProperty("Subject") 
            as string;
            this.lblMessage.Text = 
        this.EmbeddedUserControlLoader1.GetControlProperty("Message") 
            as string;
        }
    }
}

就是这样。您几乎可以像处理自定义控件一样轻松地访问了您的属性。

高级技术:自定义控件包装器

好的,有些人可能对 EmbeddedUserControlLoader.GetPropertyValue() 不满意,我可以理解。在某些情况下,例如商业分发您的控件时,您希望在 Visual Studio 2005 中获得完整的AppDesigner支持。

我们将通过在 ControlLibrary 项目中创建一个轻量级的自定义控件包装器来处理这个问题。创建一个自定义控件 FeedbackControlWrapper.cs

using System.Web.UI;
namespace ControlLibrary
{
    [ToolboxData("<{0}:FeedbackControlWrapper runat=server>
                </{0}:FeedbackControlWrapper>")]
    public class FeedbackControlWrapper : EmbeddedUserControlLoader
    {
        public FeedbackControlWrapper()
        {
            ControlClassName = "FeedbackControl";
            AssemblyName = "ControlLibrary";
            ControlNamespace = "ControlLibrary";
        }

        public string Name
        {
            get
            {
                return GetControlProperty("Name") as string;
            }
        }

        public string Subject
        {
            get
            {
                return GetControlProperty("Subject") as string;
            }
        }

        public string Message
        {
            get
            {
                return GetControlProperty("Message") as string;
            }
        }
    }
}

关注点

  • 该包装器是一个真正的自定义控件,它使用上面在 Feedback.aspx.cs 中显示的相同的属性访问器技术,但将其隐藏在控件的最终用户面前。请注意,ToolboxData 控件前缀是 FeedbackControlWrapper。它应该与类名匹配,并且应与 UserControl 类名不同。(一种替代命名约定是为包装器名称保存 SomeControl 名称模式,并为底层 UserControls 使用不同的模式,以免其他人看到)。
  • 创建可接受的控件设计时呈现超出了本文的范围,但 EmbeddedUserControlLoader 展示了您需要做什么才能创建设计时呈现。通常,HttpContext 不可用,必须对其进行模拟。任何商业控件都必须比我在本文中做得更好地处理设计时呈现问题!
  • 您可以向包装器添加属性,这些属性将出现在 VS2005 属性设计器框中。搜索您最喜欢的关于如何在设计时公开自定义控件属性的文章。

新的自定义控件反馈页面与之前的示例几乎相同。但是这一次,我们从 VS2005 工具箱中拖放我们的自定义控件,并将代码隐藏更改为更熟悉的内容。

FeedbackCustomControl.aspx.cs

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

namespace DemoSite
{
    public partial class FeedbackCustomControl : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        protected void Button1_Click(object sender, EventArgs e)
        {
            this.Panel1.Visible = true;
            this.lblName.Text = this.FeedbackControlWrapper1.Name;
            this.lblSubject.Text = this.FeedbackControlWrapper1.Subject;
            this.lblMessage.Text = this.FeedbackControlWrapper1.Message;
        }
    }
}

成功,并且架构更加专业!

高级主题:迁移 VS2005 UserControls

希望迁移 UserControl 文件的用户应注意 @Control 属性中 CodeBehindCodeFile 之间的区别。CodeBehind 是正确的属性,但 CodeFile 是所有在 SP1 之前的 VS2005 中创建的 UserControls 使用的。

请参阅 http://msdn2.microsoft.com/en-us/library/d19c0t4b.aspx 上的更多详细信息

历史

  • 2006 年 12 月 17 日 - 初稿
  • 2007 年 1 月 7 日 - 通过各种用户评论得到改进

致谢

本文受到了以下来源的启发

关于 Benjamin Allfree

Benjamin Allfree 经营着 Launchpoint Software Inc.,这是一家技术无关的公司,专门提供无与伦比的定制软件解决方案。Benjamin 大部分时间都在研究架构和新技术。

© . All rights reserved.