wind

a blog of erpcrm

 

[转发]利用 ASP.NET 2.0 中的 Web 部件和个性化释放站点的潜能

利用 ASP.NET 2.0 中的 Web 部件和个性化释放站点的潜能

作者:Steven A. Smith
相关技术:ASP.NET 2.0、门户、Web部件
难度:★★★☆☆
读者类型:ASP.NET开发人员

    [导读]本文介绍了ASP.NET 2.0中引入的新特性——Web部件,通过这个门户框架,开发人员能够快速地实现基于门户的应用,包含了Web部件的创建、安全设置、个性化门户等等方面的内容。

    假设您需要构建一个Intranet站点,以便使您所在组织中的每个人都可以基于他们的登录来查看报告和其他信息。同时假设每个用户都能够个性化该站点——添加和删除他们感兴趣的模块,以及自定义像天气或新闻来源这样的内容,从而适合他们的所在地和兴趣。现在,请考虑一下如何通过ASP.NET 1.1和Visual Studio .NET 2003在不使用任何第三方软件或工具的情况下完成这项工作。您认为该工作将花费多少时间?您需要为此编写多少行代码?几乎可以肯定,这两个问题的答案都意味着需要花费的时间和精力远超过您或您客户的预期。ASP.NET 2.0和Visual Studio 2005将通过个性化的门户解决方案来解决该问题。

解决方案

    尽管ASP.NET 2.0离正式发布还有一些时间,但我们已经可以得到Beta1。尽管该版本和最终发布版本可能有一定的偏差,但它包含了一个强大的IDE和许多激动人心的功能,包括对个性化门户的支持。而且,使用这些新功能中的许多功能时,只需要编写少量(或者根本不需要编写)额外的代码,因为所有必要的设置和属性都以声明方式配置。通过ASP.NET 2.0,您可以在数小时之内(在某些情况下只需要几分钟)解决我刚才说到的问题,并且只需要编写几行代码(如果有的话)。

    在我告诉您ASP.NET 2.0如何使这一切成为可能之前,让我先澄清开发人员在第一次听到这些信息时可能会提出的两个担忧。第一个担忧是“噢,不,我要失业了”。在您意识到仍然有大量工作有待开发人员去完成,以便使Web应用程序增值或者满足客户需要之前,这是可以理解的。ASP.NET 2.0能够帮助您为客户提供更多的价值,因为您将花费较少的时间来完成这些常见解决方案需要的相同的重复性任务。这样,您就可以将更多的时间用于来做一些有趣的工作。第二个担忧可能是“噢,不,如果我无法编写代码,就不能足够灵活地满足我的需要。”幸而,您仍然可以按照自己喜欢的方式编写代码以扩展这些功能——您只是不需要编写太多的代码,尤其是考虑到每个功能的默认或典型用法。

五分钟建立门户应用程序

    如果您安装了Visual Studio 2005并且还没有完成这一工作,那么我鼓励您完成下面的任务。我将构建一个具有我已经介绍过的所有功能的门户,并且只需要花费大约五分钟。(如果按照我的说明进行,您可能会多花费些时间,但您将会惊讶于为实际需要完成的工作是如此之少。)


图1 新站点向导

    首先,启动Visual Studio 2005并选择“File | New Web Site”。这将启动New Web Site Wizard,它为每种语言都提供了多个选项,如图1所示。在默认位置选择ASP.NET Intranet站点,并将其命名为“MSDN_Portal”。请注意,无须设置IIS Web或虚拟目录,或者指定HTTP地址。单击“OK”按钮之后,将创建一个基本站点,如图2 所示。请注意,当浏览该门户时,Visual Web Developer Web Server将在随机端口上启动。该Web服务器附带了Visual Studio 2005,从而消除了在开发人员的计算机上运行IIS的需要。出于安全原因,只有来自本地计算机的请求才会由该服务器处理。


图2 简单的主页

    无论您是否相信,您已经完成了。该应用程序包含您需要的所有内容。正如您看到的那样,该站点包含我的登录信息(在此例中,为ISENGARD域中的Administrator帐户),它是从其Windows身份验证功能中获取的。站点布局被封装在单独的文件中,以便添加到该站点的新页面可以轻松地共享同一外观。最后,用户可以重新排列和自定义主页的不同部分(并且每个用户都可以单独修改并长久保持它们)。这些部分(如,Welcome、Announcements和My Weather)中的每一个都是Web部件。如果您曾经使用过SharePoint Portal Server,则可能很熟悉Web部件。Web部件将被构建到ASP.NET 2.0中。

门户功能

    该门户一旦构建,就提供了许多在使用ASP.NET 1.x的情况下需要编写大量代码才能获得的功能。这些功能包括内置的授权和身份验证、基于单个用户的自定义、通用布局、基于XML站点映射文件的动态菜单等等。门户站点的上述功能中最激动人心的一个就是Web部件以及它们允许进行的自定义,下面让我们详细考察一下这些功能。

    Web部件允许单个用户自定义页面。支持此类自定义的页面只需公开一个链接,以便用户可以将该页面置于Edit模式。进入Edit模式之后,该用户可以修改单个Web部件的设置,并且可以添加、删除Web部件,以及通过在页面上拖放它们来重新排列Web部件。图3 阐明了该门户中的拖放功能,以及我刚刚构建的默认门户应用程序的自定义标题和天气信息。请观察向下拖动到右下角的“Steve's Links”下方的透明“Kent OH Weather”Web部件。

    用户在完成对Web部件及其在页面上的位置的更改之后,单击End Personalization链接可以使站点返回其正常状态。


图3 自定义一个页面

    从开发人员的观点来看,向现有页面添加Web部件是很简单的,因为它们只是一种通常寄宿在WebPartZone控件内部的特殊类型的用户控件。每个包含Web部件的页面还需要一个WebPartManager控件。WebPartZone控件可用于在页面上布置Web部件,以及控制它们的布局、外观和颜色。图3 中显示的门户包含两个WebPartZone控件:Left Zone和Right Zone。

    在任何使用Web部件的页面上都需要WebPartManager控件,该控件负责为使Web部件工作而需要完成的大量管线工作。它可以处理事件、绑定以及Web部件之间的通讯,并可以调用页面中Web部件的正确方法,以确保生成并正确呈现它们的控件树。该控件不具有用户界面,如果它是默认门户,则该控件被简单地声明为以下形式:

<asp:webpartmanager id="Webpartmanager1" runat="server">
</asp:webpartmanager>

    WebPartManager还可以用来在Edit模式和Normal模式之间切换页面(使用它的SetDisplayMode方法)。

创建Web部件

    编写自己的Web部件是很容易的。实际上,您可以使用三种不同的方式来完成这一工作。如果您仅在一个页面上需要Web部件,则只须将一个ContentWebPart Web控件添加到该页面,并将内容添加到该控件的ContentTemplate中。然而,您通常希望构建可以在多个页面上重用的Web部件。这可以通过用户控件或通过从System.Web.UI.WebPart继承的自定义控件来完成。有关使用自定义控件方法的Web部件的示例,请参阅该门户应用程序的/src文件夹中的WeatherWebPart源代码。

    在大多数情况下,您可能从用户控件创建Web部件。为了说明这一点,我将创建一个“Hello, User”Web部件,它可以自定义以显示用户选择的任何姓名。首先,使用内容“Hello,”和一个用于姓名的Label控件来创建一个新的.ascx文件。创建一个局部变量_YourName,类型为String。将_YourName的默认值设置为“YourName”,然后创建一个名为YourName的公共属性,以便获取和设置_YourName的值。向该类中添加Personalizable和WebBrowsable属性。接下来,在包含页面上注册该用户控件(在Visual Studio .NET中,将该控件拖动到页面的设计图面上)。将用户控件放到WebPartZone控件内,然后在浏览器中查看该页面。
要更改Web部件的文本,请将页面更改到Edit模式,然后编辑该Web部件。YourName应该被列为该控件的自定义设置之一。将YourName的值更改为您喜欢的值之一,然后切换回Normal视图模式。

    您应该看到您的问候语(经过个性化以适合您的需要)。设想一下,成千上万个用户能够自定义您的Web站点的任何一个部分,您就可以对该功能所具有的潜能产生一些了解了。该用户控件的最终代码显示在图4 中。

<%@ control language="VB" classname="Greeting"%>
<script runat="server">
Private _yourName As String = "YourName"

    <Personalizable()> <WebBrowsable()> _
    Public Property YourName() As String
        Get
            Return _yourName
        End Get
        Set(ByVal Value As String)
            _yourName = value
        End Set
    End Property

    Sub Page_PreRender(ByVal sender As Object, _
        ByVal e As System.EventArgs)
        UserNameLabel.Text = YourName
    End Sub
</script>
Hello
<asp:Label ID="UserNameLabel" Runat="Server" />

图4 用户控件

安全控件

    除了Web部件以外,该门户还可以通过使用新增的内置安全控件来提供零代码身份验证。这些控件提供相应的功能,使用户可以登录、查看他们的登录状态、恢复他们的密码,或者只是显示他们的登录用户名。在ASP.NET 1.x中,所有这些功能都需要自定义代码;但是,现在可以轻松地通过高度可自定义的方式来执行这些常见任务,并且只需要少量代码或者根本不需要任何代码。Keith Brown在本期他的文章中深入讨论了这些控件,但图5 列出了这些控件并简要介绍了它们的功能。所有这些安全控件都具有对外观的支持,从而可以容易地对其进行自定义,以适合特定应用程序或页面的外观。

控件 描述
CreateUserWizard 显示一个用户注册表单向导
Login 显示一个登录名和密码的对话框,同时包含记住登录用户密码的支持(通过cookie)
LoginName 显示当前登录用户的用户名
LoginStatus 显示登录或者注销,主要依赖于用户的状态
LoginView 允许显示隔离的内容,主要依赖于用户是否通过身份验证
PasswordRecovery 给用户提供一个向导,主要用户取回忘记的密码或者重置密码

图5 内建的安全控件

自定义门户

    当然,还需要完成很多工作来自定义示例门户,以供真正的应用程序使用。首先,站点的外观需要更新,最起码标头中的My Company Name标题需要更新。通过打开Site.master文件,可以轻松地修改默认门户的常规布局和外观。ASP.NET 2.0支持母版页,它们提供了可视化继承。页面可以从母版页继承它们的主要外观。母版页可以从其他母版页继承。对一个母版页进行的修改可以立即在使用该母版页的每个页面上生效。

    可视化继承的这一实现的两个最佳功能是Visual Studio 2005内的IDE支持以及实现的简易性。指定母版页就像将Master="site.master"参数添加到Page属性一样简单,并且您可以在web.config中指定默认的应用程序母版页。通过IDE支持可以对site.master页进行WYSIWYG编辑,并且在使用该母版页的任何页面上获得该母版页布局的只读视图。图6 以设计模式显示了default.aspx页面。请注意标头和左侧导航栏中的灰色区域。这些区域是从母版页显示的,因此无法直接从default.aspx页进行编辑(尽管右键单击并选择“Edit Master”可以打开母版页以进行编辑)。


图6 设计状态下的default.aspx页面

    除了直接编辑母版页以修改站点外观以外,单个页面还可以修改主题—这是ASP.NET 2.0中的另一项新功能,它提供了一种为站点定义多个不同外观的简便方法。主题由大量外观组成,其中每个外观都描述了特定Web控件的外观。当然,也可以轻松地从头创建主题和外观,但ASP.NET 2.0还将附带一些全局主题。现在可以使用的主题有两个:BasicBlue和SmokeAndGlass。这些主题在安装ASP.NET时被复制到%SystemRoot%\InetPub\wwwroot\system_web\[version]\Themes文件夹中。将“theme=SmokeAndGlass”添加到default.aspx页面的指令中会产生完全不同的外观。要创建您自己的外观和主题,只须配置单个Web控件的外观。实际上,它使用的语法完全相同。

    门户系统的一个常见要求是用户能够通过自己选择外观来个性化站点的外观。这一功能以及其他大量功能是通过ASP.NET 2.0的个性化功能实现的。

个性化门户

    正如前面提到的那样,“个性化”是指用户修改应用程序以适合他们的需要的能力。理想情况下,此类修改将持续存在,以便用户无须在每次访问该站点时重新应用他所作的更改。现在,您已经了解了用户如何通过在每个页面上重新排列Web部件来修改门户应用程序。这些更改会被存储并保留,以供日后访问时使用。除了修改和移动Web部件以外,用户还可能被要求为站点或单个页面选择默认主题。

    ASP.NET 2.0中的个性化功能非常强大并且易于使用。可视个性化只是冰山的一角。因为个性化遵循提供程序模型,所以很容易扩展ASP.NET 2.0中的个性化支持,以便跟踪有关单个用户的各种数据,并将这些数据保存在已经为其编写了提供程序的任何数据存储中。

    ASP.NET 2.0个性化支持经过身份验证的用户以及匿名用户的个性化。对个性化数据的访问是通过Profile对象(可以从当前的HttpContext中使用该对象)执行的。与Session和其他状态存储区不同的是,Profile对象是强类型的,并且仅根据需要读取信息,从而使得通过它来进行开发更加容易和高效。将应用程序配置为使用个性化以及指定在个性化引擎中存储哪些类型的内容,是通过在web.config文件中添加一个新节完成的。

配置个性化

    为了利用ASP.NET 2.0中的个性化支持,您必须首先对其进行配置。幸运的是,对于Intranet示例Web应用程序而言,一切已经为您准备好了,默认情况下,它将用户数据存储在/Data文件夹中的ASPNetDB.mdb文件中。要将个性化引擎配置为使用其他数据源(如SQL Server),您可以使用Web Administration工具或ASP.NET SQL Server Setup Wizard。

    在撰写本文时,我一直在使用的ASP.NET 2.0预发布版本尚未实现Web Application Administration工具的个性化部分。此刻,如果您要将SQL Server配置为个性化的提高程序,则需要使用aspnet_regsql.exe命令行工具。该工具还可用于SQL Server 2000的SQL Cache无效化计划测试功能的成员身份和设置。它既可以在命令行模式下运行(在命令行中输入所有详细信息),也可以在GUI模式下运行。以GUI模式运行该向导非常简单,并且在您为它提供了要配置的SQL Server安装的名称和连接详细信息以后,该向导将运行必要的SQL脚本,以便设置个性化所需的表和其他对象。

    在设置了数据源之后,下一个任务是针对个性化配置ASP.NET应用程序。这涉及到编辑web.config文件以及在<system.web>元素内部添加新元素,如下所示:

<profile>
    <properties>
        <add name="NickName" />
        <add name="College" type="System.String" />
        <add name="BirthDate" type="System.DateTime" />
    </properties>
</profile>

    请注意,配置文件中的每个子元素都默认为System.String类型,但您也可以显式设置该类型。通过在元素内部指定…元素,还可以将属性划分为不同的组。

存储和检索配置文件数据

    在当前用户的配置文件中存储信息是很简单的,因为现在可以直接从任意ASP.NET页访问Profile对象。与其他使用密钥/值对并且将所有内容都存储为Object的状态容器(如Session和Cache)不同,Profile对象是强类型的,并且将所有支持的属性作为该类的实际属性公开。因此,要将当前用户的NickName属性设置为从文本框传入的值,使用以下代码:

Profile.NickName = TextBox1.Text;

    Profile对象的另一个了不起的地方是它具有完全的IntelliSense?语句结束支持。您只须在Visual Studio中键入“Profile.”,就可以在一个弹出对话框中显示它所有的可用属性和方法,而无须回去浏览web.config以查看您赋予特定属性的名称。

    从Profile对象中检索数据就与向该对象中添加数据一样简单。例如,要在一个标签中显示用户的BirthDate,您可能会执行以下操作:

Label1.Text = Profile.BirthDate.ToShortDateString();

    请再次注意,与Session或Cache集合不同,Profile会返回一个强类型的对象,因此不需要进行强制转换。

匿名个性化

    通常,个性化在用户登录应用程序之后才起作用,因此可以存储该用户的首选项并将其附加到该用户的用户凭据中。然而,有时您希望在不要求用户事先登录的情况下跟踪特定用户的信息。这对于电子商务商店而言是一种常见要求,以便允许用户在不登录的情况下浏览项目并向其购物车中添加项目,然后在用户准备结帐时收集所有需要的信息。ASP.NET 2.0个性化功能通过匿名个性化为这种方案提供支持。

    匿名个性化在默认情况下被禁用,因此需要进行一些配置更改才能设置该功能。必须启用匿名标识,然后必须对配置文件中每个将支持匿名用法的属性进行显式配置。在我的当前示例基础上进行构建,添加对匿名访问购物车的支持,所得到的代码如图7 所示。

<system.web>

    <anonymousIdentification enabled="true"/>

    <profile>
        <properties>
            <add name="NickName" type="System.String"/>
            <add name="College" type="System.String"/>
            <add name="BirthDate" type="System.DateTime"/>
            <add name="Cart"
                type="Msdn.ShoppingCart, Msdn.ShoppingCart"
                serializeAs="XML"
            />
        </properties>
    </profile>

    . . .
</system.web>

图7 访问购物车

    请注意serializeAs属性,它在此例中被设置为Msdn.ShoppingCart对象的XML。它默认为String,但也可以设置为XML、Binary或ProviderSpecific,以支持所使用的任何持久性存储方法。

    默认情况下,anonymousIdentification使用Cookie来跟踪匿名用户。然而,如果用户的浏览器不支持Cookie,则存在包括AutoDetect选项(cookieless="AutoDetect")在内的受支持的备用办法,以便在支持Cookie的情况下使用Cookie,否则在URL中存储匿名ID。最后,匿名标识支持两个事件,您可以处理这两个事件以提供对该过程的附加控制。

    在创建匿名ID时,将引发AnonymousIdentification_OnCreate事件。它允许生成的匿名ID被改写,以防您需要使用自定义方案来指定匿名用户的ID。在匿名用户进行身份验证(因此不再是匿名用户)时,将引发AnonymousIdentification_OnRemove事件。其用途在于使您可以清理与该匿名ID相关联的任何数据。

    只通过此方式配置站点设置,匿名用户将能够在其配置文件中存储他们的个人信息,并且在多次请求和访问(从同一台计算机操作,并假设他们尚未清除他们的Cookie)之间持久保留该数据。但是,匿名个性化的另一项必要的功能是:一旦用户注册或登录,就能够将设置从匿名配置文件中传输到经过身份验证的配置文件中。这是通过处理Personalization_OnMigrateAnonymous事件完成的,该事件在AnonymousIdentification_OnRemove事件之后引发。要在匿名用户登录(可能在结帐期间)后立即迁移该用户的购物车,您可以在global.asax文件中使用如下内容的代码:

void Personalization_OnMigrateAnonymous(Object sender,
    PersonalizationMigrateEventArgs e)
{
    Profile.Cart = Profile.GetProfile(e.AnonymousId).Cart;
}

个性化与其他状态存储区之比较

    个性化引擎与其他状态存储机制相比,会如何呢?可以在Profile对象和Session对象之间进行明显的比较,因为这两个对象都具有基于单个用户的作用域。Profile对象提供了几个超越Session对象的关键优点,从而使其成为存储您知道将在设计时需要的用户信息的理想位置。

    Profile对象是强类型的,从而使其性能超越Session对象(每当从Session中读取对象时,它都要求进行显式强制转换)。Profile对象还使得在集成开发环境中工作变得更加容易,在这里,语句结束和编译时检查可确保访问正确的值,而不是像Session对象那样使用简单的字符串值作为密钥。

    Profile数据在用户会话结束后仍然会保存下来,并且可以存储在多种数据存储区中。另一方面,Session状态仅持续到用户在某个时间段内不活动为止。此外,Profile数据仅在需要时才会读取,而Session数据每当用户请求时都会被请求。通过将页面指令EnableSessionState按照页面需要设置为false或readonly,可以逐个页面地优化Session用法。)Session数据还会在用户已经停止使用应用程序之后相当长一段时间内继续使用内存资源,而Profile数据却不会导致该内存被占用。

    由于存在上述消极因素,因此Session并不具有多少优点。个性化要求进行一些配置和设置,但只须进行一次并且工作量极小。Session只在默认情况下才能工作(尽管它支持配置)。Session数据通常比Profile数据具有更快的读写速度(假设它是使用默认的InProc设置进行配置的),这是因为它只存储在内存之中。但是,如果您要使用状态服务器或SQL Server来存储会话状态,则该优点将完全消失。

个性化提供程序

    默认情况下,个性化使用Microsoft Access作为它的数据存储区,尽管可以将其配置为使用SQL Server以及由用户编写的自定义提供程序。提供程序只是一个类,它负责将配置文件数据从Web应用程序移动到某种数据存储区中(反之亦然)。提供程序设计模式是ASP.NET 2.0版本的一项关键功能,并且可以在许多功能中看到。可以使用自定义提供程序将Profile数据映射到现有的Microsoft数据库(而不是使用个性化SQL架构),或者引用在默认情况下不受支持的数据存储区(如Oracle或XML文件)。在同一应用程序中可以使用多个提供程序。每个属性都可以指定一个用于访问它的提供程序,或者可以将属性划分为不同的组,并且将每个组与单独的提供程序相关联。

    例如,下面的配置将使用默认的AspNetAccessProvider提供程序来存储某些用户数据,并使用AspNetSqlProvider来存储其他某些数据。请注意,每个提供程序都定义在machine.config文件中,而且AspNetAccessProvider被用作默认的提供程序,这只是因为它首先出现在machine.config文件中。建议您让每个应用程序通过在元素上设置其默认提供程序来指定该提供程序,如图8 所示。

<profile defaultProvider="AspNetAccessProvider">
    <properties>
        <group name="Commerce">
            <add name="Cart"
                type="Msdn.ShoppingCart, Msdn.ShoppingCart"
                serializeAs="XML"
                provider="AspNetSqlProvider"
            />
        </group>
        <group name="UserInfo">
            <add name="NickName"/>
            <add name="College"/>
        </group>
    </properties>
</profile>

图8 个性化提供程序

    图8中的UserInfo数据将通过AspNetAccessProvider(它是默认提供程序)进行访问,而Commerce数据将通过AspNetSqlProvider来访问。即使您将对所有Personalization设置只使用单个提供程序,将类似的项目组合在一起也是很有用的,因为这样可以在Profile对象上按组来引用它们。例如,要在图8 中定义的College属性中存储某些信息,您可以使用类似于以下内容的代码:

Profile.UserInfo.College = CollegeTextBox.Text;

自定义提供程序

    除了ASP.NET 2.0附带的提供程序以外,您还将能够编写自己的提供程序。这是相当简单的,并且可以使某些新增功能(如个性化和成员身份)可以由比采用其他方式时多得多的用户使用,因为许多组织不愿意使用新的数据存储来存储该信息。通过编写您自己的提供程序,您可以完全控制数据的存储方式和检索方式,从而实现个性化。要在备用数据存储区(如Web服务器上的XML文件)中存储配置文件数据,您必须创建一个从System.Web.Profile.ProfileProvider类继承的类。该基类提供了一些常用功能,并且指定了您的自定义提供程序(例如,XmlProfileProvider)为了作为Profile功能的提供程序工作而需要实现的所有属性和方法。遗憾的是,XmlProfileProvider的完整实现超出了本文的范围,但在编写该提供程序之后,下面的配置片段就可以演示如何引用它:

<profile defaultProvider="XmlProfileProvider">
    <providers>
        <add name="XmlProfileProvider"
            type="Msdn.XmlProfileProvider, XmlProfileProvider"
        />
    </providers>
    <properties>
        <add name="NickName"/>
        <add name="College"/>
    </properties>
</profile>

    元素的元素中的类型属性应该格式化,以便它能够指定提供程序的完全限定类名,并且后面跟它所在的程序集的名称(不带.dll扩展名),该程序集应该位于应用程序的/bin文件夹中或全局程序集缓存中。

小结

    ASP.NET 2.0门户框架是有关多种新增功能如何协同工作以提供强大、完整的解决方案的卓越示例。新增的安全控件使用户身份验证变得轻而易举。母版页和主题使得管理站点中所有页面的外观变得非常容易。Web部件提供了一种强大的方式,使用户可以个性化站点以适合他们的个人需要,而ASP.NET 2.0的个性化/配置文件功能提供了易于使用的API,以便跟踪应用程序内部有关单个用户的详细信息。很显然,Microsoft的ASP.NET团队已经通过许多能够提高工作效率的功能构建了这一通用的Web应用程序原型,这些功能可以协同工作以产生整体效果,而不是仅仅将其各个(Web)部件简单地组合在一起。

    作者简介:Steven A. Smith是AspAlliance.com和DevAdvice.com这两个面向.NET的开发人员社区的主席。他是ASP.NET Developer's Cookbook (SAMS, 2003)的作者之一,并且正在撰写2.0版。他是一名Microsoft ASP.NET MVP,并且通过他的公司 ASPSmith.com 提供有关.NET的培训。

posted on 2005-09-11 08:09  WIND  阅读(1289)  评论(0编辑  收藏

导航

统计

ECubeCMS