精通
英语
和
开源
,
擅长
开发
与
培训
,
胸怀四海
第一信赖
I migrated a part of a big site (> 500 pages) to DotNetNuke which is an Open Source .NET CMS. After migration, the loading of each page was very slow (10 sec!). After investigating, I found out that the SiteMap (a TreeViewcontrol) was the source of the time delay. So, I tested three other SiteMap controls, which I found on the Internet. All had nearly the same result, they were much too slow. My first thought was that the reading of 500 pages was time consuming. So I made a test program, and found out that the reading from the DB was finished in no time, the building of a tree data structure with pages as nodes (with parents) was finished in no time, even the filling of the .NET 2.0 web TreeView control was very quick. The bottleneck was the rendering of the MS web TreeViewcontrol. I assume that the other, slower, SiteMaps always filled the complete tree, which then took the TreeViewcontrol to render that long, even if most of the nodes were collapsed. And that was my idea, to speed that process up because you normally have only a small bit of your tree expanded, you simply don't need the data from other leaves, because no one is watching them. And if the user expands a node, then I get the children from the DB and fill just that one node. Another feature of DnnSiteMap is that it realizes on which page, which is called a Tab in DNN slang , it is, and expands itself to that node and selects it.我把一个大网站的部分页面(至少500个页面)升级到DotNetNuke平台下。在升级后,每个页面加载非常慢(10秒)。经过调查,我查出SiteMap(TreeViewcontrol)是主因。所以,我测试了3个其它的SiteMap控件。3个几位结果一样,它们都很慢。第一个想法是500个页面的读取比较耗时。所以我写了个测试程序,从DB读取会很快完成,树形数据构造很快,甚至填充.NET 2.0 web 树形控件也很快。瓶颈在于web 树形控件的渲染。我假定慢的SiteMaps总是完整填充树形,完整填充会让树形控件渲染时间长,即使大部分节点是折叠的。基于通常只需要加载部分节点,为了加快处理,简化过程,不需要其它叶子节点的数据,因为没人看这些节点。如果用户展开节点,那么我从DB里获取子节点信息,填充到展开节点下。DnnSiteMap的另外特点是依赖了DNN的Tab控件,Tab会展开自己到选中节点下,且Tab会被先用。
Important: DnnSitemap is for DotNetNuke 4 and APS.NET 2.0 only! For installation, you must follow the install instructions (see below).DnnSitemap在DotNetNuke 4 和 APS.NET 2.0平台提供。请遵守如下安装说明。
DotNetNuke is an open source .NET Content Management System. It is derived from the IBuySpyPortal, which is a best practice from Microsoft to show the capabilities of ASP.NET. Currently, DotNetNuke (DNN) is in version 4 which is based on the new ASP.NET 2.0 and is programmed in VB.NET. Because of its big community support, MS is supporting the DNN project. DNN is programmed by a core team, lead by Shaun Walker.
I did a bit of over-commenting inline of the code, so that anyone can understand what each step is doing. Basically, the code is straightforward. All the data and the business logic is in the App_Code folder, and the UI code is in the ViewDnnSiteMap.ascx.cs file.
In the data layer, you will find the following functions:
/// <summary> /// Gets all tabs that have no ParentId /// and are not deleted and visible /// </summary> /// <returns>IDataReader: TabId (int), TabName (string), /// Children (int)</returns> public abstract IDataReader GetRootNodesFromDb(); /// <summary> /// Gets all tabs that are children of the specified tab /// </summary> /// <param name="parentTabId">TabId of the parent tab</param> /// <returns>IDataReader: TabId (int), TabName (string), /// Children (int)</returns> public abstract IDataReader GetChildNodesFromDb(int parentTabId); /// <summary> /// Gets parent tab for specified tab /// </summary> /// <param name="childTabId">TabId of the child tab</param> /// <returns>IDataReader: ParentTabId (int), ParentName (string); /// (should be max one row)</returns> public abstract IDataReader GetParentFromDb(int childTabId); /// <summary> /// Gets the Tab, that hosts the given module /// </summary> /// <param name="tabModuleId">TabModuleId of the module</param> /// <returns>IDataReader: ParentTabId (int), ParentName (string); /// (should be max one row)</returns> public abstract IDataReader GetTabViaTabModuleIdFromDb( int tabModuleId); /// <summary> /// Gets node with specified TabId from Db /// </summary> /// <param name="nodeTabId">TabId for node</param> /// <returns>IDataReader: TabId (int), TabName (string), /// Children (int)</returns> public abstract IDataReader GetNodeFromDb(int nodeTabId);
You can find the implementation of these functions in the SqlDataProvider.cs file. They are basically simple SQLSELECT statements. In future releases, these will be in a stored procedure, to gain some extra performance.
The business layer can be found in the controller class in DnnSiteMapController.cs. The functions are:
/// <summary> /// Retrieves all visible root nodes from Db /// </summary> /// <returns>List of root nodes as ExtendedTreeNode /// </returns> public List<ExtendedTreeNode> GetRootNodesFromDb() /// <summary> /// Retrieves Child Nodes from Db for given Node /// </summary> /// <param name="parentNode">ParentNode, /// for which the children should be retrieved</param> /// <returns>List of children as ExtendedTreeNode /// </returns> public List<ExtendedTreeNode> GetChildNodesFromDb( TreeNode parentNode) /// <summary> /// Gets the navigation path for a given Tab to the root /// </summary> /// <param name="childTab">Tab for /// which the path should be retrieved</param> /// <returns>List of Tabs, begining with the root /// and ending with the Child</returns> public List<Structs.Tab> GetNavigationPathFromDb( Structs.Tab childTab) /// <summary> /// Gets the Tab, that hosts the given module /// </summary> /// <param name="tabModuleId">TabModuleId of the module /// </param> /// <returns>Dnn TabId</returns> public Structs.Tab GetTabViaTabModuleIdFromDb(int tabModuleId) /// <summary> /// Gets node with specified TabId from Db /// </summary> /// <param name="nodeTabId">TabId for node</param> /// <returns>Specified node; null if node is not found /// </returns> public ExtendedTreeNode GetNodeFromDb(int nodeTabId)
The UI code is in the ViewDnnSiteMap.ascx.cs file. In the Page_Load event, the settings are applied and the root nodes are retrieved from the DB. Then, the tree expands to the current tab (the page which is hosting the control):
protected void Page_Load(System.Object sender, System.EventArgs e) { try { if (!IsPostBack) { // controller class DnnSiteMapController objDnnSiteMaps = new DnnSiteMapController(); // config settings ConfigurationSettings settings = new ConfigurationSettings(this.Settings); // set show lines this.TreeView1.ShowLines = settings.ShowLines; // set image set this.TreeView1.ImageSet = settings.ImageSet; // set node wrap this.TreeView1.NodeWrap = settings.NodeWrap; // set show controls this.pnlControls.Visible = settings.ShowControls; // set node indent this.TreeView1.NodeIndent = settings.NodeIndent; // fill root nodes or specified rootNode this.FillRootNodes(settings.RootNode); // get current TabId from DNN and expand to it this.ExpandToTab(this.TabId); } } catch (Exception exc) //Module failed to load { Exceptions.ProcessModuleLoadException(this, exc); } }
In the TreeNodeExpanded event, I check if the node already has its children, if not, I retrieve them from the DB:
protected void TreeView1_TreeNodeExpanded(object sender, TreeNodeEventArgs e) { // if node has DummyNode, else data was // already retrieved from Db if (NodeHelper.HasDummyNode(e.Node)) { // controller class DnnSiteMapController objDnnSiteMaps = new DnnSiteMapController(); // clear child nodes e.Node.ChildNodes.Clear(); // for all child nodes foreach (ExtendedTreeNode childNode in objDnnSiteMaps.GetChildNodesFromDb(e.Node)) { // if root has children, add dummy node if (childNode.HasChildren) { NodeHelper.AddDummyNode(childNode); } // add children to expanded node e.Node.ChildNodes.Add(childNode); } } // select current node this.SelectCurrentNode(); }
The private ExpandToTab(int tabId) method is used in the Page_Load event. This method is used to expand the tree to a specified node and select it. This is very handy in the Page_Load event, because you can set the tab to the current page:
private void ExpandToTab(int tabId) { // controller class DnnSiteMapController objDnnSiteMaps = new DnnSiteMapController(); // collapses all nodes; IMPORTANT to use this function // instead directly TreeView.CollapseAll(), because // it can loose all nodes this.CollapseAll(); // find node in tree view (no roundtrip to Db) TreeNode node = NodeHelper.GetNode(this.TreeView1.Nodes, tabId); // check if node is already in tree view if (node != null) { TreeNode currentNode = node; // expand to node while (currentNode != null) { currentNode.Expand(); currentNode = currentNode.Parent; } } else // get parent path from Db { List<Structs.Tab> parentTabs = objDnnSiteMaps.GetNavigationPathFromDb( new Structs.Tab(tabId, String.Empty)); TreeNode currentNode = null; // expand all nodes along path foreach (Structs.Tab nodeTab in parentTabs) { currentNode = NodeHelper.GetNode(this.TreeView1.Nodes, nodeTab.TabId); if (currentNode != null) { currentNode.Expand(); } } } // select current node this.SelectCurrentNode(); }
Login in as host, and select in the Host menu "Module Definitions". At the bottom of the page, press "Upload new module". Then, select the .zip file, add it, and then upload it.
The module is written in C#, you must include the following lines in your web.config file, in the <system.web> <compilation> section (it should already be there, but commented out):
<codeSubDirectories> <add directoryName="DnnSiteMap"/> </codeSubDirectories>
Because every tab (page) in DNN has a TabId, I save that information with every TreeNode. For that, I use thevalue field. It is of type string, so, I convert it to int whenever needed.
The ExtendedNode class is derived directly from the System.Web.UI.WebControls.TreeNode class. It adds the boolean field HasChildren to the class. This is used when I retrieve the nodes from the DB; then I set that flag to true, if they have child nodes. With this little trick, I can spare an extra roundtrip to the DB.
The TreeView control shows a plus sign to expand a node, if it has at least one child node. Because I don't want to retrieve nodes as long as the parent is expanded, I fill them with a DummyNode. To differentiate from a normal node, I set their value field to -1.
I encapsulated the settings in the class ConfigurationSettings. It reads the settings out of a Hashtable (e.g.,ModuleSettingsBase.TabModuleSettings or PortalModuleBase.Settings), makes them type-safe, and initializes them with default values.
New features can be: using stored procedures; defining custom CSS classes .... So, there is still lots to do.