www.dnnworkflow.cn

随笔- 20  文章- 4  评论- 32 
2009年1月16日

用Python和Google AppEngine开发基于Google架构的应用软件

    在研究了一段时间的Google AppEngine之后,我发现Google AppEngine是未来一个非常不错的开发平台,希望可以尽快搞清楚GoogleAppEngine整个的平台架构,并且可以尽快基于这个平台出一个像样的东西——学习任何语言或者平台的唯一一个好办法就是赶紧拿过来就用,千万不要等,一等就过去太长时间,错过了机会。不要以为学了放在那里是一种“储备”,没有使用,就不会有储备。

    到目前为止,我认为Google的Python开发平台、BigTable、Django都是非常不错的工具,组合起来之后是一个非常强大的开发利器。

    Python是一个非常先进的、跨平台的强大的开发语言,豆瓣的后台就是使用Python进行开发的;BigTable是Google的后台存储架构,Google的部分软件就是基于BigTable进行存储的,Google的存储机制自动完成负载均衡和数据库的优化,并且在数据库的结构上就已经支持了这种网络的架构;Django是一个基于Python和关系型数据库的Web快速开发支持包,由于Google的BigTable并不是关系型数据库,所以原则上来说DJango并不支持Google的架构,但是由于Google做了从BigTable到一般关系型数据库的转换,所以DJango已经可以在Google架构下稳定的执行。

    关于BigTable,不应该将之理解为简单的关系型数据库——当然,BigTable也确实不是关系型数据库,而是Google公司基于自己的网络结构而研发的“互联网”数据库。那么,BigTable和一般的关系型数据库相比,其优点在哪儿呢?我们以一个企业的应用为例,如果一个企业应用了关系型数据库,无论是SQL Server、DB2还是其他的关系型数据库,都要考虑当数据量极大的时候数据如何均衡访问的情况,对于企业来说,必须要在现有的网络架构上扩充其架构,才能使之不断的适应越来越海量的数据要求。但是单表的数据仍然可能是极大的,比如一张表有几百万条记录,这个时候,整个系统的效能一定是非常低的。BigTable从其设计上来说,就是为了适应互联网的海量数据的,而不仅限于企业级的应用,因此其存储能力、查询能力的设计上是更加强大的。我们知道,企业级应用中,我们所面临的用户是“有限”的、可预计的;而对于互联网的应用
,我们所面临的用户则几乎是无法预计的,其行为也有一定的不确定性,从而给数据库带来了更大的风险。举一个不太恰当的例子,当一个突发事件发生的时候——比如当年克林顿总统“拉链门”的斯塔尔报告就曾经几乎让整个互联网瘫痪。之所以说这个例子不太恰当,是因为我们在当下的互联网上所需要做的,是云计算之下的面向全球的企业级应用,而不仅仅是若干个文档的下载。

    BigTable所做的事情之一就是存储结构上的优化——当然,由于其是基于互联网架构的,我们没有办法在自己的Windows或者Linux上安装一个BigTable来分析其底层结构——而且Google应该也不允许我们这么做。BigTable数据库系统从其根本上并不是为了将其数据放在企业内部而设计的(这也就是为什么Google AppEngine不希望将Devappserver.py开发的程序作为企业应用的理由之一,因为开发环境下根本不是真正意义上的BigTable,而只是一个文件模拟存储而已。)——另一个可能的原因就是除了Google之外,世界上可能没有其他适合BigTable运行的硬件架构,众所周知,Google的底层硬件结构是自创的。

    Google的BigTable的存储是实体的概念,我们关系型数据库传统意义上的“表”对BigTable来说是一个实体集合;而表中的每一行则是一个“实体”。对于单个的实体而言,“字段”——当然,这里所说的字段是对应于关系型数据库而言的,在BigTable中,我们称之为属性(Property),BigTable的属性可能不是确定数量的,如果对于一些实体来说,有五个属性,那么对于另外一些实体则可能有十个属性,这完全是根据需要而定的,不必为所有的实体都预先分配相同的属性。这种设计对于一般数量级的应用来说似乎无关紧要,因为关系型数据库本身就已经对“空”数据进行了优化处理;但是对于海量数据和系统的扩展而言就有很重要的意义了。BigTable的另外一个特性就是它的存储是可以“分组”的。例如,对于我们开发的一个系统,我们可能希望在一个事务中一次性处理一个用户的数据(这是非常可能的,可能来自于用户的操作,也有可能是因为系统的升级而需要对数据分别进行批量处理),如果有这种需要的话,我们可以将数据进行分组,分组的方法就是在创建一个实体的时候将另外一个实体作为新实体的父节点。已经创建的实体不能对其重新进行分组。分组后的数据,在Google后台的存储是存储在基础架构的同一个节点(注意,对Google而言的一个“节点”显然不是我们传统意义上的一台单机,而是一组后台存储架构,可能包含上千台服务器)上的。正是因为同一分组的数据放在同一个存储节点上,于是才可以在同一个事务中进行处理。

    除了GoogleAppEngine提供的以上三利器之外,为了更好的开发富应用的互联网系统,还必须要引入客户端的Ajax,比如Prototype或者Dojo等,从而可以使系统和用户的交互更加友好。

posted @ 2009-01-16 14:49 DnnWorkflow 阅读(2362) 评论(8) 编辑
2008年11月17日

    因为有些变动,挺累,很久没更新。

    这一篇纯粹是充数的烂鱼。

    DotNetNuke的登录界面可以保存用户名和密码,但是,一旦保存之后,就连密码也不用输入了,这样不太安全;一般的做法是记住用户名,让用户自行修改密码比较好一些,这只需要修改两个文件,分别是:
    

    \admin\Authentication\Login.ascx.vb:一旦登录成功,将用户名写入到Cookies中

 1         Private Sub ValidateUser(ByVal objUser As UserInfo, ByVal ignoreExpiring As Boolean)
 2             Dim validStatus As UserValidStatus = UserValidStatus.VALID
 3             Dim strMessage As String = Null.NullString
 4             Dim expiryDate As DateTime = Null.NullDate
 5 
 6             If Not objUser.IsSuperUser Then
 7                 validStatus = UserController.ValidateUser(objUser, PortalId, ignoreExpiring)
 8             End If
 9 
10             UserId = objUser.UserID
11 
12             'Check if the User has valid Password/Profile
13             Select Case validStatus
14                 Case UserValidStatus.VALID
15 
16 
17                     'Y 如果验证通过,则写Cookies,将用户名写到Cookies中
18                     Dim myCookie As New System.Web.HttpCookie("mycookies", objUser.Username)
19                     myCookie.Expires = DateTime.Now.AddDays(10)
20                     Response.Cookies.Add(myCookie)
21                     'End of Y
22 
23 
24                     'Set the Page Culture(Language) based on the Users Preferred Locale
25                     If (Not objUser.Profile Is NothingAndAlso (Not objUser.Profile.PreferredLocale Is NothingThen
26                         Localization.SetLanguage(objUser.Profile.PreferredLocale)
27                     Else
28                         Localization.SetLanguage(PortalSettings.DefaultLanguage)
29                     End If
30 
31                     'Set the Authentication Type used 
32                     AuthenticationController.SetAuthenticationType(AuthenticationType)
33 
34                     'Complete Login
35                     UserController.UserLogin(PortalId, objUser, PortalSettings.PortalName, AuthenticationLoginBase.GetIPAddress(), chkCookie.Checked)
36 
37                     ' redirect browser
38                     Response.Redirect(RedirectURL, True)
39                 Case UserValidStatus.PASSWORDEXPIRED
40                     strMessage = String.Format(Localization.GetString("PasswordExpired"Me.LocalResourceFile), expiryDate.ToLongDateString)
41                     AddLocalizedModuleMessage(strMessage, ModuleMessageType.YellowWarning, True)
42                     PageNo = 2
43                     pnlProceed.Visible = False
44                 Case UserValidStatus.PASSWORDEXPIRING
45                     strMessage = String.Format(Localization.GetString("PasswordExpiring"Me.LocalResourceFile), expiryDate.ToLongDateString)
46                     AddLocalizedModuleMessage(strMessage, ModuleMessageType.YellowWarning, True)
47                     PageNo = 2
48                     pnlProceed.Visible = True
49                 Case UserValidStatus.UPDATEPASSWORD
50                     AddModuleMessage("PasswordUpdate", ModuleMessageType.YellowWarning, True)
51                     PageNo = 2
52                     pnlProceed.Visible = False
53                 Case UserValidStatus.UPDATEPROFILE
54                     'Admin has forced profile update
55                     AddModuleMessage("ProfileUpdate", ModuleMessageType.YellowWarning, True)
56                     PageNo = 3
57             End Select
58 
59             ShowPanel()
60 
61         End Sub

 

    \DesktopModules\AuthenticationServices\DNN\Login.ascx.vb:页面加载的时候,从Cookies中读取用户名

 1         Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
 2             '读Cookies用户名
 3             Dim myCookie As System.Web.HttpCookie
 4             myCookie = Request.Cookies.Get("mycookies")
 5             Try
 6                 txtUsername.Text = myCookie.Value
 7             Catch ex As Exception
 8 
 9             End Try
10 
11             DotNetNuke.UI.Utilities.ClientAPI.RegisterKeyCapture(Me.Parent, Me.cmdLogin, Asc(vbCr))
12 
13             If Not Request.IsAuthenticated Then
14                 If Page.IsPostBack = False Then
15                     Try
16                         If Not Request.QueryString("username"Is Nothing Then
17                             txtUsername.Text = Request.QueryString("username")
18                         End If
19                         If Not Request.QueryString("verificationcode"Is Nothing Then
20                             If PortalSettings.UserRegistration = PortalRegistrationType.VerifiedRegistration Then
21                                 'Display Verification Rows 
22                                 rowVerification1.Visible = True
23                                 rowVerification2.Visible = True
24                                 txtVerification.Text = Request.QueryString("verificationcode")
25                             End If
26                         End If
27 
28                     Catch
29                         'control not there 
30                     End Try
31                 End If
32 
33                 txtPassword.Attributes.Add("value", txtPassword.Text)
34 
35                 Try
36                     If String.IsNullOrEmpty(txtUsername.Text) Then
37                         SetFormFocus(txtUsername)
38                     Else
39                         SetFormFocus(txtPassword)
40                     End If
41                 Catch
42                     'Not sure why this Try/Catch may be necessary, logic was there in old setFormFocus location stating the following
43                     'control not there or error setting focus
44                 End Try
45             End If
46 
47             trCaptcha1.Visible = UseCaptcha
48             trCaptcha2.Visible = UseCaptcha
49 
50             If UseCaptcha Then
51                 ctlCaptcha.ErrorMessage = Localization.GetString("InvalidCaptcha", Localization.SharedResourceFile)
52                 ctlCaptcha.Text = Localization.GetString("CaptchaText", Localization.SharedResourceFile)
53             End If
54 
55         End Sub

     

把各自的函数代码一替换就完事了,哦,对了,版本是4.8.2,如果版本不对的话,自行把Cookies那一段代码剪切一下就可以了。
    
posted @ 2008-11-17 21:03 DnnWorkflow 阅读(445) 评论(1) 编辑
2008年11月3日
    对于标准的DotNetNuke模块,数据是和模块相关的,这个也就是我们在开发模块的数据结构的时候,比如像Survery模块等,主表必然是有一个 ModuleId这个字段的,这也就保证了当我们在同一个网站中即使有100个该模块的实例,但是其数据却是完全不同的,从而保证了我们可以在同一个网站 中对模块进行任意多次的重用,这个是非常好的一个特性。

    同时,DotNetNuke的模块机制确保了模块是一个完全封闭的、完全有自我行为控制能力的独立的小整体,就像是美利坚合众国下的州一样,虽然属于同一个体制之下,但是却可以有自己的法律和行为——这个就可以理解为模块的Control(控制)。

    以上所讲的可能有点啰嗦而且难懂,但是我却坚持要将之讲出来,是因为我觉得这对于我扩展DotNetNuke来说非常重要。

    对于DotNetNuke来说,每一个模块,就可以理解为一个完完全全的、完完整整的系统(在DotNetNuke大框架之内的完整),完全不依赖于其他任何的普通模块(管理模块除外)。

    当然,这也有一定的限制,也就是对于DotNetNuke来说,模块之间,基本上是没有互动的;而且,模块之间的数据,也是毫无关系的。而我在进行模块开 发的时候,认为如果模块之间完全没有关系的话,实际上可能不能反映某些实际的案例,所以,在我的数据结构中,就已经预留了相应的接口,从而确保模块之间(当然是同一类型的模块)有可能进行数据的共享,这也就是我在《DotNetNuke自定义窗体模块的数据结构(二)》 的数据结构中,除了常规的ModuleId之外,还要增加FormID和KeyID的缘故,我希望的是开发出来的模块既可以做到像通常的 DotNetNuke模块那样,有本模块完全独立的数据,也可以在管理员配置的情况下,不但可以展示本模块实例的数据,同时也可以展示统一模块下其他模块 实例的数据。

    标准模式下,DotNetNuke的模块和数据之间的关系应该是如下图所示:


        经过数据结构的重新设计之后,我们的一个模块既可以对应自己本模块的数据,也可以在某种情况下,将其他模块的数据“拿出来”一起进行展示。按照我们的数据结构,DotNetNuke的模块和数据的关系就如下图所示:

    上图中,模块实例01和02仍然是只展示自己的数据;但是模块实例03则既可以展示本模块的数据,又将01、02两个模块的数据取出来一起进行展示。

    这样的好处是什么呢?我个人认为,在业务逻辑非常复杂的情况下,如果数据非要用一个模块来展示的话,可能会造成权限的 极其复杂,甚至会造成权限的混乱;而如果模块之间的数据可以共享的话,那么,复杂的业务逻辑,就可能可以拆分成若干个模块,这若干个模块每一个专注某一项 功能,和页面的权限、模块的权限组合在一起,可以较为轻松的完成在一个模块中很难完成的业务逻辑。

    嗯,仍然是有点难以理解,需要晚一点,我们会涉及到这种情况的例子,到时候我们再回来看这一章节。
posted @ 2008-11-03 20:12 DnnWorkflow 阅读(1502) 评论(1) 编辑
2008年11月1日
    在接触国外的CMS等Open Source产品之前,老实说,我写过的存储过程,包括SQL Server的、Oracle的,加起来绝对不会超过5个,而且还基本上都是从网上抄袭的,主要是觉得太麻烦:嗯,是的,如果数据库设计的不够好,经常需 要改动,比如加一个字段,修改一下字段类型的话,需要从数据表、存储过程、调用等一路改上来,确实是挺麻烦的。不过习惯了之后,发现用存储过程确实有它的 好处,也被迫养成了对数据库设计要每个字段都斟酌半天的习惯,所以,我们看到DotNetNuke有近乎上千个存储过程(安装了所有的附加模块之后)。

    我现在非常喜欢使用存储过程的方式来操作数据库——不过,诚如我们在《DotNetNuke自定义窗体模块的数据结构(二)》中所说的那样,由于我们修改了数据结构,所以,存储过程在这里使用是不太适合的。唉,好吧,我声明:我所说的并不代表我的意思,我对我说的话不承担任何责任 :(

    我们在(二)中重新把UDT的UserDefinedRows这张表(在我们的模块中已经改名了)做了修改,如果是希望在列表上显示的,或者是参与查询 的,我们都作为基本字段放在列表中;而其他的字段则作为扩展字段放在扩展表中,在用户进行定义的时候,可以选择是使用基本字段,还是扩展字段。当然,在这 种情况之下,基本字段就不可能被“用完”,在某一个Form之中,我们可能只需要基本字段中的几个而已,其他剩余的我们都不需要用到,简而言之,也就是我 们需要更新的字段是运行时决定的,而不是设计时决定的。而这个时候,如果我们在进行INSERT或者UPDATE操作的时候,是对所有的字段都操作一遍的 话,不但是有点怪,而且也容易造成错误,比如,日期型、整数型等的字段,本来明明是没有内容的,但是我们非要INSERT入内容,会出现错误,或者是不正 确的初始数据,我们希望,没有使用到的字段,就保留其为NULL就好了,不需要对该字段进行操作(大家可以插入空值或者是String.Empty试试, 会觉得很头疼)。

    所以,我们需要在运行时生成SQL语句,我们把这个函数放在下面,供大家参考。
 1         Public Overrides Function AddHashRow(ByVal field As Hashtable) As Integer
 2             Dim sqlComm As SqlCommand = New SqlCommand()
 3 
 4             Dim strSql As String = "INSERT INTO RedstartFormRow ("
 5 
 6             Dim strField As String = ""
 7             Dim strParm As String = ""
 8 
 9             For Each dic As DictionaryEntry In field
10                 If strField <> "" Then
11                     strField = strField & ""
12                 End If
13                 strField = strField & dic.Key.ToString()
14 
15                 If strParm <> "" Then
16                     strParm = strParm & ""
17                 End If
18                 strParm = strParm & "@" & dic.Key.ToString()
19 
20                 sqlComm.Parameters.Add(New SqlParameter("@" & dic.Key.ToString(), GetNull(dic.Value.ToString())))
21             Next
22             strSql = strSql & strField & ") VALUES (" & strParm & ")"
23 
24             strSql = strSql & "; select SCOPE_IDENTITY()"
25 
26             Dim sqlConn As New SqlConnection()
27             sqlConn.ConnectionString = ConnectionString
28             sqlConn.Open()
29             sqlComm.Connection = sqlConn
30 
31             sqlComm.CommandText = strSql
32 
33             Return CType(sqlComm.ExecuteScalar(), Integer)
34         End Function

    这个函数,是放在SqlDataProvider.vb文件中的。
    而按照DotNetNuke通常的方式,是使用存储过程来和数据库打交道的,也就是如下图所示:
    在 我们的处理中,没有将数据库的操作交给存储过程来完成,而是在SqlDataProvider里面,就是用SqlCommand来执行SQL语句,执行动 态的SQL插入动作。使用存储过程当然可以获得更高的执行效率,不过,通过SqlCommand也是可以的方式。除了INSERT动作之外,我们的 Update动作也是动态的,仍然是通过动态拼凑SQL语句的方式,对数据表进行更新。而在这个函数中,我们仍然调用select SCOPE_IDENTITY(),以便可以马上返回我们的值,以方便我们在程序中可能的马上要对这一条数据进行继续处理。

    在上述的方式中,我们使用到了哈希表来存储数据,哈希表的Key对应我们的字段名称,而Value则对应用户表单中输入的内容。于是,在我们的动态窗体页面上,当页面进行保存的时候,我们需要将所有的用户控件的内容读取到哈希表中,然后再调用本函数,进行插入,或者更新。

    这只是我们的基本字段的部分,除了基本字段,我们还有扩展字段,扩展字段是对UDT的UserDefinedData表(对于我们来说,是RedstartFormRowData表)进行插入,或者更新。这个就非常简单了,我们使用存储过程来实现就可以了。

    好了,大家还记得我们曾经在《基于DotNetNuke的动态窗体支持(二)》这篇文章中定义了一个EditControls的集合吗?这个集合中包含了我们所有的用户自定义控件。OK,我们在保存的时候,需要做的事情就是
    1、遍历EditControls集合,找到所有的基本字段,将之送到一个哈希表中。然后,调用我们上面的AddHashRow的函数去保存基本字段;
    2、再遍历一遍EditControls集合,基本字段置之不理,找到扩展字段,然后逐个的调用我们保存扩展字段的存储过程,将之保存到扩展表中;

    以上就是我们对自定义窗体结构的保存过程,在这里,我没有为我们的数据结构定义对应的对象,因为我觉得,对于这样的结构,定义对象并不太好,当然,这就导致了IDataReader的大量使用,也出现了一些问题,到时候我们再来讲。
posted @ 2008-11-01 00:02 DnnWorkflow 阅读(993) 评论(3) 编辑
2008年10月29日
 

    06年底的时候,关于UDT的结构和其他的一些问题,和Sebastian Leupold通过邮件,因为当时费了很大的劲才弄明白数据到底是怎么给弄出来的,DotNetNuke里面像这么难懂的代码还是不太多的,我的建议是把 UDT的数据结构稍微改一下,弄成让大家更舒服一点的,不过估计是大家交流上还是有点障碍,毕竟E文都不是母语(SL是德国佬);后来想:算了,还是自己 弄吧,于是从这个数据结构出发,把它给优化了一下,成了我的数据结构,希望不会让大家觉得太烂。

    首先再贴一张图,分析一下UserDefinedTable的情况
(原来的数据结构存储)

    我把三张表的数据弄到一块了,这样大家可以看得更清楚一点。上图中可以看到,UserDefinedFields是一 个字段定义器存放的数据,比如我们的字段名称、初始值、顺序等,这个表可以理解为一个“结构表”;而下面的UserDefinedRows是“主记录表 ”,UserDefinedData是“从记录表”。我们的表格结构是存放在Fields表里面的,而所有的表格内容,都是存放在Rows和Data这两 张表中的。

    大家可以看到,UserDefinedRows这张表只有两个字段,其实,除了几乎每个用户开发模块都有的ModuleId(是为了在不同的模块中显示不同的数据),那么,就只有一个字段,也就是UserDefinedRowID,这张表太浪费了!

    而我们再回头想一下我们通常使用的列表/窗体,其实,对于“列表/窗体”这种模式来说,显示在列表上的必然只是有限的几个字段而已,而大量的字段,是不需 要显示在列表上的,所以,我们的改造就是,把需要在列表上显示的,尽量的放在UserDefinedRows这张表中,那么,其实,我们读取数据的时候, 就不需要弄那么复杂的LEFT JOIN,也不需要在内存中动态生成DataTable了,这种方式,当然是可以大大提高执行效率的。改进后的数据结构,可能是如下图所示的:

    OK,其实看过这张图大家应该知道我的想法了,就是把数据分成两部分,一部分是需要(或者是可能需要)在列表上进行展示的(还有,比如要在查询条件中出现 的、要参与系统查询接口的等等),我们尽量将之放在UserDefinedRows这张表中;另外一部分是只是在打开表格中才会用到或者读取的,我们将之 放在UserDefinedData这张表中。这个时候,如果我们再要读取数据的话,那就非常简单的,可能的SQL语句就是如下这种样式的:

     SELECT UserDefinedID, Field1 AS 姓名, Field2 AS 性别, Field3 AS 年龄 FROM UserDefinedRows

    只是在编辑或者查看详细的内容的时候,我们才需要去读取UserDefinedData这张表,所以,效率上肯定不会有问题。既然我们对这个数据结构作了 修改,那么,字段定义表UserDefinedFields这张表的结构当然也需要修改一下,至少要标明哪些字段是“基本字段”(也就是在 UserDefinedRows中出现的字段),哪些是扩展字段(也就是在UserDefinedData中出现的字段),其他的,根据我们的业务需要去 进行扩展就可以了。

    在让用户进行表单定义的时候,可以告诉用户,如果需要在列表上显示和查询的,就尽量使用基本字段;其他的,则任意定义。

    通过这种方式,我们把UserDefinedTable这个模块往前提升了一步,仍然支持无限的字段定义,但是,在缺省的Control(也就是View界面)上读取数据的时候,我们尽量抛弃UDT原来的非常让人头疼的方式。

    为了让数据更加规范,我们在增加UserDefinedRows表的字段的时候,做严格的数据类型限制(而不需要像 UDTData表那样统一的采用ntext类型,或者是varchar类型),这样的好处就是存储的数据是绝对严格的,程序出错的几率更小。如果大家用 varchar字段存储过日期格式、数值格式,而又碰到过需要对这些字段进行拼凑SQL语句查询的话,大家就明白我在说什么了,那种出错的痛苦(数据格式不规范,如本来应该是日期型,结果实际存储的不是,于是,日期转换函数报错),简直就是让人欲哭无泪。

    所以……,是的,在大家崩溃之前,我要告诉大家,我已经把UserDefinedRows的两个字段扩展到几十个字段 了,就是上图这个庞然的数据结构,看到其中的ParentID了?是的,我让这个玩意儿也支持树形结构了!如果大家有足够的耐心给我足够的时间的话,应该可以看到我的解释。(还有FormID、KeyID等,也需要解释)

    反倒是UserDefinedData这张表,和原来没什么太大的不同,这里我们就不贴图了。

    好了!为了遍历和查询的快捷性以及准确性,我们把UserDefinedRows这张表给扩展了,增加了数据结构,并且要求严格限制数据类型;而为了支持动态窗体的无限字段支持,我们仍然保留了UserDefinedData这张表。不要LEFT JOIN,不要在内存里面动态生成DataTable,那都是太浪费效率的方式。

    弄完数据结构之后,马上我们就面临一个问题了,那就是用存储过程是不太好更新我们的数据结构的,所以,下面我们要做的 事情,就是把DotNetNuke传统的使用存储过程保存数据的方式也改掉(当然,仍然没有动及DotNetNuke的开发结构,这个是我们不能动的)。 我们下一部分来谈这个问题,我们将会使用哈希表的结构,来动态的更新这张表。
posted @ 2008-10-29 11:37 DnnWorkflow 阅读(1623) 评论(3) 编辑
2008年10月28日
posted @ 2008-10-28 23:52 DnnWorkflow 阅读(1353) 评论(4) 编辑
2008年10月27日
posted @ 2008-10-27 21:03 DnnWorkflow 阅读(976) 评论(3) 编辑
posted @ 2008-10-27 15:54 DnnWorkflow 阅读(980) 评论(0) 编辑
posted @ 2008-10-27 12:58 DnnWorkflow 阅读(1373) 评论(4) 编辑
2008年10月25日
posted @ 2008-10-25 00:17 DnnWorkflow 阅读(205) 评论(1) 编辑