editing web.sitemap programatically

T

Tim Mackey

hi,
i have put my web.sitemap in /App_Data so i can edit it programatically via
a web admin page, inheriting the modify permissions from the App_Data folder
etc.

i was hoping the provider would simply persist any changes i made, but it
doesn't happen.
e.g. selected.ParentNode.ChildNodes.Remove(selected);
throws a NotSupportedException "Collection is read-only"

in a previous project i dug into the xml of the document and managed the
thing via XML classes. is there no better way?

thanks
tim
 
C

Cowboy \(Gregory A. Beamer\)

The sitemap is a special file that gets locked. If you want a fully editable
file, you should institute you own file and system.
 
T

Tim Mackey

hi
thanks for the reply. although i don't really think my scenario calls for a
custom file and system. the only custom part is some very basic xml
operations on the existing file, with which i can continue to use the
existing site map classes. the framework automatically reloads the sitemap
file when it changes.
i was just hoping there was a better way than manually editing the xml nodes
in the file, it is a real shame to lose the object-orientedness of the
provider as soon as you want to change the data.

tim
 
S

Steven Cheng[MSFT]

Thanks for Gregory's input.

Hello Tim,

I think Gregory means you can do some slight customization on the built-in
XmlSitemapProvider class to meet your current requirement. The existing
limitation of the existing XmlSiteMapProvider class is that it will
contruct a SiteMapNode tree(from root to leaf) at the first time
application request it, after that, it cached the Node Tree in memory for
performance improvement. However, this makes the siteMapNode tree
unchangable unless the ASP.NET application(appdomain) be restarted. That's
why when you modify the sitemap (web.sitemap) file in your application, the
sitemap tree won't change according to your modification in sitemap file.
here is the code logic from diassembled reflector code:

======the method it initialize the SiteMap Node tree=====

public override SiteMapNode BuildSiteMap()
{
SiteMapNode node1 = this._siteMapNode;
if (node1 != null)
{
return node1;
}

............................
//here it construct the tree from sitemap file

}
=================================

Based on my research, there are two possible approaches for you to
customize this sitemap provider's behavior:

1. define a subclass derived from the built-in XmlSiteMapProvider, and
define a new method(called "UpdateSiteMapTree"), in this method, you can
first call the parent class's "Clear" method(a protected function) which
will remove the original cached SiteMap Node tree. Then, you can call the
"BuildSiteMap" method to let it reconstruct the sitemap tree from file.

2. MS has provided the source code of most built-in providers of ASP.NET
2.0(include SQL membership&role&profile providers and the XMLSitemap
provider). You can download it below. Thus, you can completely recompile a
custom one without inheriting the built-in one.

http://download.microsoft.com/download/a/b/3/ab3c284b-dc9a-473d-b7e3-33bacfc
c8e98/ProviderToolkitSamples.msi

Hope this helps.

Sincerely,

Steven Cheng

Microsoft MSDN Online Support Lead



==================================================

Get notification to my posts through email? Please refer to
http://msdn.microsoft.com/subscriptions/managednewsgroups/default.aspx#notif
ications.



Note: The MSDN Managed Newsgroup support offering is for non-urgent issues
where an initial response from the community or a Microsoft Support
Engineer within 1 business day is acceptable. Please note that each follow
up response may take approximately 2 business days as the support
professional working with you may need further investigation to reach the
most efficient resolution. The offering is not appropriate for situations
that require urgent, real-time or phone-based interactions or complex
project analysis and dump analysis issues. Issues of this nature are best
handled working with a dedicated Microsoft Support Engineer by contacting
Microsoft Customer Support Services (CSS) at
http://msdn.microsoft.com/subscriptions/support/default.aspx.

==================================================



This posting is provided "AS IS" with no warranties, and confers no rights.
 
T

Tim Mackey

hi steven,
thanks for the tip.
just for anyone else who is interested, i've included my implementation
below. it consists of a derived XmlSiteMapPRovider as suggested by steve,
and an accompanying web admin page, allowing view/edit/add/delete of the
sitemap nodes. it assumes unique titles in all pages, which may not be true
for other apps, the code could easily be modified to specify a key or other
attribute. i haven't time to make it totally user friendly, i.e. removing
my own functions etc., but it shouldn't be too hard to adapt. i'm very
surprised this functionality wasn't bundled, surely it is a popular feature
to programitcally change the web.sitemap file?

anyway, here is web admin page aspx code:

<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1"
runat="Server">
<h2>
Site Map / Navigation / Permissions</h2>
<div class="FloatRightPanel">
<h3>
<asp:Label ID="lblAddEdit" runat="server"
Text="Add"></asp:Label>
URL: </h3>
<br />
<strong>Title:</strong>
<asp:TextBox ID="txtTitle" runat="server" ValidationGroup="User"
Columns="40"></asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1"
runat="server" ControlToValidate="txtTitle"
Display="Dynamic" ErrorMessage="* Required"
SetFocusOnError="True"
ValidationGroup="User"></asp:RequiredFieldValidator><br />
<strong>URL: </strong>&nbsp;<asp:TextBox ID="txtUrl" runat="server"
ValidationGroup="User"
Columns="40">~/</asp:TextBox>
<br />
<strong>Parent Node:</strong>
<asp:DropDownList ID="ddlNodes" runat="server" DataTextField="Title"
DataValueField="Title">
</asp:DropDownList><br />
<strong>Roles:&nbsp;</strong><asp:CheckBoxList ID="chkRoles"
runat="server" RepeatDirection="Horizontal"
RepeatColumns="2">
</asp:CheckBoxList>&nbsp;
<br />
<asp:Button ID="btnSave" runat="server" Text="Save"
ValidationGroup="User" OnClick="btnSave_Click" />
<asp:Button ID="btnCancel" runat="server" Text="Cancel"
OnClick="btnCancel_Click" />
<asp:Button ID="btnDelete" runat="server" Text="Delete"
OnClick="btnDelete_Click"
CausesValidation="False" />
</div>
<asp:GridView Style="float: left" CssClass="grid"
HeaderStyle-CssClass="gH" AlternatingRowStyle-CssClass="gA"
RowStyle-CssClass="gI" SelectedRowStyle-CssClass="gS" ID="GridView1"
runat="server"
AutoGenerateColumns="False"
OnSelectedIndexChanged="GridView1_SelectedIndexChanged">
<Columns>
<asp:CommandField ShowSelectButton="True" />
<asp:BoundField DataField="Url" HeaderText="URL" ReadOnly="True"
SortExpression="Url" />
<asp:BoundField DataField="Title" HeaderText="Title"
ReadOnly="True" SortExpression="Title" />
</Columns>
<RowStyle CssClass="gI" />
<SelectedRowStyle CssClass="gS" />
<HeaderStyle CssClass="gH" />
<AlternatingRowStyle CssClass="gA" />
</asp:GridView>
</asp:Content>






web admin page code behind:

using System;
using System.Data;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Configuration;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

public partial class System_Administration_SiteMap : System.Web.UI.Page
{
private SiteMapDataSource SiteMap;

protected void Page_Load(object sender, EventArgs e)
{
this.SiteMap = (this.Master as DefaultMaster).SiteMapDs; // use the
master page SiteMapDataSource rather than recreate multiple ones for child
pages etc.
if(!IsPostBack)
{
this.ddlNodes.DataSource = this.GridView1.DataSource =
this.SiteMap.Provider.RootNode.GetAllNodes();
this.GridView1.DataBind();
this.ddlNodes.DataBind();
this.ddlNodes.Items.Insert(0, this.SiteMap.Provider.RootNode.Title);
this.chkRoles.DataSource = Roles.GetAllRoles();
this.chkRoles.DataBind();
this.btnDelete.Visible = false;
}
}

private SiteMapNode GetSelectedGridViewNode()
{
string SelectedTitle = this.GridView1.SelectedRow.Cells[2].Text;
foreach(SiteMapNode child in this.SiteMap.Provider.RootNode.GetAllNodes())
if(child.Title == SelectedTitle)
return child;
return null;
}

private SiteMapNode GetSelectedDdlParentNode()
{
foreach(SiteMapNode node in this.SiteMap.Provider.RootNode.GetAllNodes())
if(node.Title == this.ddlNodes.SelectedValue)
return node;
// if the code falls out to here, it must be the root node selected
return this.SiteMap.Provider.RootNode;
}

protected void GridView1_SelectedIndexChanged(object sender, EventArgs e)
{
// find the node and init the edit controls
this.lblAddEdit.Text = "Edit"; // switch to 'edit' mode
SiteMapNode selected = GetSelectedGridViewNode();
if(selected == null)
{
WebUtil.JavascriptAlert("Error: node not found", this);
return;
}
this.txtTitle.Text = selected.Title;
if(!String.IsNullOrEmpty(selected.Url)) // get the correct
(unmapped/changed) url by reading the xml file directly
this.txtUrl.Text = (this.SiteMap.Provider as
EditableXmlSiteMapProvider).GetActualUrl(selected.Title);
else
this.txtUrl.Text = "";
foreach(ListItem item in chkRoles.Items)
if(selected.Roles.Contains(item.Text))
item.Selected = true;
this.ddlNodes.SelectedValue = selected.ParentNode.Title;
this.btnDelete.Visible = true;
}

protected void btnSave_Click(object sender, EventArgs e)
{
string roles = "";
foreach(ListItem item in chkRoles.Items)
if(item.Selected)
roles += item.Text + ",";
roles = roles.TrimEnd(',');

if(this.lblAddEdit.Text == "Add")
(this.SiteMap.Provider as
EditableXmlSiteMapProvider).AddNode(GetSelectedDdlParentNode().Title,
this.txtTitle.Text.Trim(), this.txtUrl.Text.Trim(), roles);
else
(this.SiteMap.Provider as
EditableXmlSiteMapProvider).UpdateNode(GetSelectedGridViewNode().Title,
this.ddlNodes.SelectedValue, this.txtTitle.Text.Trim(),
this.txtUrl.Text.Trim(), roles);
WebUtil.JavascriptAlertRedirect("Record saved", Request.Url.PathAndQuery);
}

protected void btnDelete_Click(object sender, EventArgs e)
{
(this.SiteMap.Provider as
EditableXmlSiteMapProvider).DeleteNode(GetSelectedGridViewNode().Title);
WebUtil.JavascriptAlertRedirect("Record deleted",
Request.Url.PathAndQuery);
}

protected void btnCancel_Click(object sender, EventArgs e)
{
Response.Redirect(Request.Url.PathAndQuery);
}
}





XmlSiteMapProvider code:

using System;
using System.Data;
using System.Configuration;
using System.IO;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Configuration;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml;

/// <summary>
/// Extends the XmlSiteMapProvider class with edit functionality
/// Requires all siteMapNodes to have unique titles.
/// </summary>
public class EditableXmlSiteMapProvider : XmlSiteMapProvider
{
public EditableXmlSiteMapProvider()
{
}

public void AddNode(string parentTitle, string title, string url, string
roles)
{
XmlDocument doc = LoadXmlDoc();
XmlElement parent = FindNodeByTitle(doc, parentTitle);

XmlElement newChild = doc.CreateElement("siteMapNode",
"http://schemas.microsoft.com/AspNet/SiteMap-File-1.0");
newChild.SetAttribute("url", url); // url must go in lower case to get
xpath to work
newChild.SetAttribute("title", title); // url must go in lower case to get
xpath to work
newChild.SetAttribute("roles", roles);
parent.AppendChild(newChild);
SaveXmlDoc(doc);
}

public void UpdateNode(string originalTitle, string newParentTitle, string
title, string url, string roles)
{
XmlDocument doc = LoadXmlDoc();
XmlElement node = FindNodeByTitle(doc, title);

node.SetAttribute("url", url); // url must go in lower case to get xpath
to work
node.SetAttribute("title", title); // url must go in lower case to get
xpath to work
node.SetAttribute("roles", roles);

// check if the parent has changed
if(node.ParentNode.Attributes["title"].Value != newParentTitle)
{
node.ParentNode.RemoveChild(node);

// find the new parent
XmlElement newParent = FindNodeByTitle(doc, newParentTitle);
newParent.AppendChild(node);
}
SaveXmlDoc(doc);
}

public void DeleteNode(string title)
{
XmlDocument doc = LoadXmlDoc();
XmlElement node = FindNodeByTitle(doc, title);
node.ParentNode.RemoveChild(node);
SaveXmlDoc(doc);
}

private XmlDocument LoadXmlDoc()
{
XmlDocument doc = new XmlDocument();
doc.Load(HttpContext.Current.Server.MapPath(FilePath));
return doc;
}

private void SaveXmlDoc(XmlDocument doc)
{
string AbsPath = HttpContext.Current.Server.MapPath(FilePath);
try
{
doc.Save(AbsPath);
}
catch(UnauthorizedAccessException ex)
{
try
{
// try to remove 'read-only' attribute on the file.
WebUtil.RemoveReadOnlyFileAttribute(AbsPath);
doc.Save(AbsPath);
}
catch
{
throw ex; // throw the original exception
}
}
base.Clear();
base.BuildSiteMap();
}

private XmlElement FindNodeByTitle(XmlDocument doc, string title)
{
string xPath = String.Format("//*[@title='{0}']", title);
XmlElement node = doc.SelectSingleNode(xPath) as XmlElement;
if(node == null)
throw new Exception("Node not found with title: " + title);
return node;
}

/// <summary>
/// The built in SiteMapNode.Url property gives a different value to the
actual web.sitemap value,
/// it is a mapped value that changes depending on the v.dir of the running
web site.
/// This method reads the xml attribute direct from the web.sitemap file.
/// </summary>
public string GetActualUrl(string title)
{
return this.FindNodeByTitle(LoadXmlDoc(), title).Attributes["url"].Value;
}

public static string FilePath
{
get
{
// if anyone gets a nicer method to read the web siteMapFile attribute,
please post it. i tried using System.Web.Configuration but i couldn't get it
working. also, the site breaks when running off VS web server because of
'cannot read IIS metabase' errors.
string webConfigText =
File.ReadAllText(HttpContext.Current.Server.MapPath("~/web.config"));
Match m = Regex.Match(webConfigText, @"siteMapFile=""(.*?)""",
RegexOptions.IgnoreCase | RegexOptions.Multiline);
if(!m.Success)
return "/web.sitemap"; // default value. otherwise we could throw new
Exception("web.config does not contain a siteMapFile element");
else
return m.Groups[1].Captures[0].Value;
}
}
}


and finally, the web.config settings:
<siteMap defaultProvider="EditableXmlSiteMapProvider" enabled="true">
<providers>
<add name="EditableXmlSiteMapProvider" description="Default SiteMap
provider." type="EditableXmlSiteMapProvider"
siteMapFile="~/App_Data/web.sitemap" securityTrimmingEnabled="true"/>
</providers>
</siteMap>
 
S

Steven Cheng[MSFT]

Cool! Thanks for the sharing.

Sincerely,

Steven Cheng

Microsoft MSDN Online Support Lead


This posting is provided "AS IS" with no warranties, and confers no rights.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,995
Messages
2,570,230
Members
46,819
Latest member
masterdaster

Latest Threads

Top