Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]

.pdf
Скачиваний:
107
Добавлен:
16.08.2013
Размер:
29.8 Mб
Скачать

578 C H A P T E R 1 6 W E B S I T E N AV I G AT I O N

For example, the following code shows a site map that uses a target attribute to indicate the frame where the link should be opened. This technique is useful if you’re using frames-based navigation (rather than a master page), as described in Chapter 29. In this example, one link is set with a target of _blank so it will open in a new (pop-up) browser window.

<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode title="Home" description="Root" url="~/Default.aspx">

<siteMapNode title="Products" description="Our products" url="~/Products.aspx" target="_blank" />

...

</siteMapNode>

</siteMap>

Now in your code, you have several options. If you’re using a template in your navigation control, you can bind directly to the new attribute. Here’s an example with the GridView from the previous section:

<ItemTemplate>

<a href='<%# Eval("Url") %>'

target='<%# Eval("[target]") %>'><%# Eval("Title") %></a> <br />

<%# Eval("Description") %> </ItemTemplate>

The one trick in this example is that you need to use square brackets around the attribute name to indicate that the value is being looked up (by name) in the data item’s indexer.

If your navigation control doesn’t support templates (or you don’t want to create one), you’ll need to find another approach. Both the TreeView and Menu classes expose an event that fires when an individual item is bound (TreeNodeDataBound and MenuItemDataBound). You can then customize the current item. To apply the new target, you use this code:

protected void TreeView1_TreeNodeDataBound(object sender, TreeNodeEventArgs e)

{

e.Node.Target = ((SiteMapNode)e.Node.DataItem)["target"];

}

Notice that you can’t retrieve the custom attribute from a strongly typed property. Instead, you retrieve it by name using the SiteMapNode indexer.

Note You can also create a custom SiteMapProvider that returns instances of a custom SiteMapNode-derived class. However, a significant amount of extra code is required, and as a result it’s often not worth the trouble.

Creating a Custom SiteMapProvider

To really change how the ASP.NET navigation model works, you need to create your own site map provider. You might choose to create a custom site map provider for several reasons:

You need to store site map information in a different data source (such as a relational database).

You need to store site map information with a different schema from the XML format expected by ASP.NET. This is most likely if you have an existing system in place for storing site maps.

C H A P T E R 1 6 W E B S I T E N AV I G AT I O N

579

You need a highly dynamic site map that’s generated on the fly. For example, you might want to generate a different site map based on the current user, the query string parameters, and so on.

You need to change one of the limitations in the XmlSiteMapProvider implementation. For example, maybe you want the ability to have nodes with duplicate URLs.

You have two choices when implementing a custom site map provider. All site map providers derive from the abstract base class SiteMapProvider in the System.Web namespace. You can derive from this class to implement a new provider from scratch. However, if you want to keep the same logic but use a different data store, just derive from the StaticSiteMapProvider class instead. It gives you a basic implementation of many methods, including the logic for node storing and searching.

In the following sections, you’ll see a custom provider that lets you store site map information in a database.

Storing Site Map Information in a Database

In this example, all navigation links are stored in a single database table. Because databases don’t lend themselves easily to hierarchical data, you need to be a little crafty. In this example, each navigation link is linked to a parent link in the same table, except for the root node. This means that although the navigational links are flattened into one table, you can re-create the right structure by starting with the home page and then searching for the subset of rows at each level.

Figure 16-14 shows the SiteMap table with some sample data that roughly duplicates the site map you saw earlier in this chapter.

Figure 16-14. The SiteMap table

In this solution, the site map provider won’t access the table directly. Instead, it will use a stored procedure. This gives some added flexibility and potentially allows you to store your navigation information with a different schema, as long as you return a table with the expected column names from your stored procedure.

Here’s the stored procedure used in this example:

CREATE PROCEDURE GetSiteMap AS

SELECT * FROM SiteMap

GO

Creating the Site Map Provider

Because this site map provider doesn’t change the underlying logic of site map navigation, you can derive from StaticSiteMapProvider instead of deriving from SiteMapProvider and reimplementing all the tracking and navigation behavior (which is a much more tedious task).

580 C H A P T E R 1 6 W E B S I T E N AV I G AT I O N

Here’s the class declaration for the provider:

public class SqlSiteMapProvider : StaticSiteMapProvider { ... }

The first step is to override the Initialize() method to get all the information you need from the web.config file. The Initialize() method gives you access to the configuration element that defines the site map provider.

In this example, your provider needs three pieces of information:

The connection string for the database.

The name of the stored procedure that returns the site map.

The provider name for the database. This allows you to use provider-agnostic coding (as described in Chapter 7). In other words, you can support SQL Server, Oracle, or another database equally easily, as long as there’s a .NET provider factory installed.

You can configure your web application to use the custom provider (SqlSiteMapProvider) and supply the required three pieces of information using the <siteMap> section of the web.config file:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <system.web>

<siteMap defaultProvider="SqlSiteMapProvider"> <providers>

<add name="SqlSiteMapProvider" type="SqlSiteMapProvider" providerName="System.Data.SqlClient"

connectionString=

"Data Source=localhost;Initial Catalog=Northwind;Integrated Security=SSPI" storedProcedure="GetSiteMap" />

</providers>

</siteMap>

...

</system.web>

</configuration>

Now in your provider you simply need to retrieve these three pieces of information and store them for later:

private string connectionString; private string providerName; private string storedProcedure;

public override void Initialize(string name, System.Collections.Specialized.NameValueCollection attributes)

{

if (!IsInitialized)

{

base.Initialize(name, attributes);

// Retrieve the web.config settings. providerName = attributes["providerName"];

connectionString = attributes["connectionString"]; storedProcedure = attributes["storedProcedure"];

if (providerName == null || providerName.Length == 0) throw new Exception("The provider name was not found.");

else if (connectionString == null || connectionString.Length == 0) throw new Exception("The connection string was not found.");

C H A P T E R 1 6 W E B S I T E N AV I G AT I O N

581

else if (storedProcedure == null || storedProcedure.Length == 0) throw new Exception("The stored procedure name was not found.");

initialized = true;

}

}

private bool initialized = false; public virtual bool IsInitialized

{

get { return initialized; }

}

The real work that the provider does is in the BuildSiteMap() method, which constructs the SiteMapNode objects that make up the navigation tree. In the lifetime of an application, you’ll typically construct the SiteMapNode once and reuse it multiple times. To make that possible, the provider needs to store the site map in memory:

private SiteMapNode rootNode;

The root SiteMapNode contains the first level of nodes, which then contain the next level of nodes, and so on. Thus, the root node is the starting point for the whole navigation tree.

You override the BuildSiteMap() method to actually create the site map. The first step is to check if the site map has already been generated and then create it. Because multiple pages could share the same instance of the site map provider, it’s a good idea to lock the object before you update any shared information (such as the in-memory navigation tree).

public override SiteMapNode BuildSiteMap()

{

lock (this)

{

//Don't rebuild the map unless needed.

//If your site map changes often, consider using caching. if (rootNode == null)

{

//Start with a clean slate.

Clear();

...

Next, you need to create the database provider and use it to call the stored procedure that gets the navigation history. The navigation history is stored in a DataSet (a DataReader won’t work because you need back-and-forth navigation to traverse the structure of the site map).

...

//Get all the data (using provider-agnostic code). DbProviderFactory provider =

DbProviderFactories.GetFactory(providerName);

//Use this factory to create a connection. DbConnection con = provider.CreateConnection(); con.ConnectionString = connectionString;

//Create the command.

DbCommand cmd = provider.CreateCommand(); cmd.CommandText = storedProcedure; cmd.CommandType = CommandType.StoredProcedure; cmd.Connection = con;

582 C H A P T E R 1 6 W E B S I T E N AV I G AT I O N

// Create the DataAdapter.

DbDataAdapter adapter = provider.CreateDataAdapter(); adapter.SelectCommand = cmd;

// Get the results in a DataSet. DataSet ds = new DataSet(); adapter.Fill(ds, "SiteMap");

DataTable dtSiteMap = ds.Tables["SiteMap"];

...

The next step is to navigate the DataTable to create the SiteMapNode objects, beginning with the root node. You can find the root node by searching for the node with no parent (where ParentID is null). In this example, no attempt is made to check for all the possible error conditions (such as duplicate root nodes).

...

// Get the root node.

DataRow rowRoot = dtSiteMap.Select("ParentID IS NULL")[0];

...

Now to create a SiteMapNode, you need to supply the key, URL, title, and description. In the default implementation of a site map provider, the key and URL are the same, which makes searching by URL easier. The custom SqlSiteMapProvider also uses this convention.

...

rootNode = new SiteMapNode(this,

rowRoot["Url"].ToString(), rowRoot["Url"].ToString(),

rowRoot["Title"].ToString(), rowRoot["Description"].ToString());

...

Now it’s time to fill in the rest of the hierarchy. This is a step that needs to be performed recursively so that you can drill down through a hierarchy that’s an unlimited number of levels deep. To make this work, the SqlSiteMapProvider uses a private AddChildren method, which fills in one level at a time. Once this process is complete, the root node that provides access to the full site map is returned.

...

string rootID = rowRoot["ID"].ToString();

// Fill down the hierarchy. AddChildren(rootNode, rootID, dtSiteMap);

}

}

return rootNode;

}

The AddChildren() method simply searches the DataTable for records where the ParentID is the same as the current ID—in other words, it finds all the parents for the current node. Each time it finds a child, it adds the child to the SiteMapNode.ChildNodes collection using the AddNode method that’s inherited from StaticSiteMapProvider.

Here’s the complete code:

private void AddChildren(SiteMapNode rootNode, string rootID, DataTable dtSiteMap)

{

DataRow[] childRows = dtSiteMap.Select("ParentID = " + rootID); foreach (DataRow row in childRows)

{

SiteMapNode childNode = new SiteMapNode(this,

C H A P T E R 1 6 W E B S I T E N AV I G AT I O N

583

row["Url"].ToString(), row["Url"].ToString(), row["Title"].ToString(), row["Description"].ToString());

string rowID = row["ID"].ToString();

//Use the SiteMapNode AddNode method to add

//the SiteMapNode to the ChildNodes collection. AddNode(childNode, rootNode);

//Check for children in this node. AddChildren(childNode, rowID, dtSiteMap);

}

}

The only limitation in the AddChildren() method is that it doesn’t attempt to apply any sort of positioning. Instead, entries are added in the order they appear in the database. Changing this behavior isn’t difficult. To do so, you need to add a SortOrder column to the database table. Then,

you could sort the records before adding them using an overload of the DataTable.Select() method. The only remaining details are to fill a few other required overloads that retrieve the site map

information:

protected override SiteMapNode GetRootNodeCore()

{

return BuildSiteMap();

}

public override SiteMapNode RootNode

{

get { return BuildSiteMap(); }

}

protected override void Clear()

{

lock (this)

{

rootNode = null; base.Clear();

}

}

This completes the example. You can now request the same pages you created earlier, using the new site map provider (as configured in the web.config file). The custom provider plugs in easily and neatly. The new information will flow through the custom provider and arrive in your pages without any indication that the underlying plumbing has changed.

URL Mapping

In some situations, you might want to have several URLs lead to the same page. This might be the case for a number of reasons—maybe you want to implement your logic in one page and use query string arguments but still provide shorter and easier-to-remember URLs to your website users (often called friendly URLs). Or maybe you have renamed a page, but you want to keep the old URL functional so it doesn’t break user bookmarks. Although web servers sometimes provide this type of functionality, ASP.NET 2.0 now includes its own URL mapping feature.

The basic idea behind ASP.NET URL mapping is that you map a request URL to a different URL. The mapping rules are stored in the web.config file, and they’re applied before any other processing takes place. Of course, for ASP.NET to apply the remapping, it must be processing the request,

584C H A P T E R 1 6 W E B S I T E N AV I G AT I O N

which means the request URL must use a file type extension that’s mapped to ASP.NET. (See Chapter 18 for more information about how to configure ASP.NET to handle file extensions that it wouldn’t ordinarily handle.)

You define URL mapping in the <urlMappings> section of the web.config file. You supply two pieces of information—the request URL (as the attribute url) and the new destination URL (mappedUrl). Here’s an example:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <system.web>

<urlMappings enabled="true"> <add url="~/Category.aspx"

mappedUrl="~/Default.aspx?category=default" /> <add url="~/Software.aspx" mappedUrl="~/Default.aspx?category=software" />

</urlMappings>

...

</system.web>

</configuration>

To make a match, the incoming URL must be requesting the same page. However, the case of the request URL is ignored, as are query string arguments. Unfortunately, there’s no support for advanced matching rules, such as wildcards or regular expressions.

When you use URL mapping, the redirection is performed in the same way as the Server.Transfer() method, which means there is no round-trip and the URL in the browser will still show the original request URL, not the new page. In your code, the Request.Path and Request.QueryString properties reflect the new (mapped) URL. The Request.RawUrl property returns the original friendly request URL.

This can introduce some complexities if you use it in conjunction with site maps—namely, does the site map provider try to use the original request URL or the destination URL when looking for the current node in the site map? The answer is both. It begins by trying to match the request URL (provided by Request.RawUrl property), and if no value is found, it then uses the Request.Path property instead. This is the behavior of the XmlSiteMapProvider, so you could change it in a custom provider if desired.

The TreeView Control

The TreeView is the most impressive new control in ASP.NET 2.0. Not only does it allow you to render rich tree views, it also supports filling portions of the tree on demand (and without refreshing the entire page). But most important, it supports a wide range of styles that can transform its appearance. By setting just a few basic properties, you can change the TreeView from a help topic index to a file and folder directory listing. In fact, the TreeView doesn’t have to be rendered as a tree at all—it can also tackle nonindented hierarchical data such as a table of contents with the application of just a few style settings.

You’ve already seen two basic TreeView scenarios. In Chapter 12, you used a TreeView to display bound XML data. In this chapter, you used a TreeView to display site map data. Both of these examples used the ability of the TreeView to bind to hierarchical data sources. But you can also fill a TreeView by binding to an ordinary data source (in which case you’ll get only a single level of nodes) or by creating the nodes yourself, either programmatically or through the .aspx declaration.

The latter option is the simplest. For example, by adding <asp:TreeNode> tags to the <Nodes> section of a TreeView control, you can create several nodes:

C H A P T E R 1 6 W E B S I T E N AV I G AT I O N

585

<asp:TreeView runat="server"> <Nodes>

<asp:TreeNode Text="Products"> <asp:TreeNode Text="Hardware"/>

</asp:TreeNode>

<asp:TreeNode Text="Services"/> </Nodes>

</asp:TreeView>

And here’s how you can add a TreeNode programmatically when the page loads:

TreeNode newNode = new TreeNode("Software");

//Add as a child of the first root node

//(the Products node in the previous example). TreeView1.Nodes[0].ChildNodes.Add(newNode);

When the TreeView is first displayed, all the nodes are shown. You can control this behavior by setting the TreeView.ExpandDepth property. For example, if ExpandDepth is 2, only the first three levels are shown (level 0, level 1, and level 2). You can also programmatically collapse and expand nodes by setting the TreeNode.Expanded property to true or false.

This just scratches the surface of how a TreeView works. To get the most out of the TreeView, you need to understand how to customize several other details for a TreeNode.

The TreeNode

Each node in the tree is represented by a TreeNode object. As you already know, every TreeNode has an associated piece of text, which is displayed in the tree. The TreeNode object also provides navigation properties such as ChildNodes (the collection of nodes it contains) and Parent (the containing node, one level up the tree). Along with this bare minimum, the TreeNode provides all the useful properties detailed in Table 16-9.

Table 16-9. TreeNode Properties

Property

Description

Text

The text displayed in the tree for this node.

ToolTip

The tooltip text that appears when you hover over the node text.

Value

Stores a nondisplayed value with additional data about the node (such as a

 

unique ID you’ll use when handling click events to identify the node or look

 

up more information).

NavigateUrl

If set, it automatically forwards the user to this URL when this node is clicked.

 

Otherwise, you’ll need to react to the TreeView.SelectedNodeChanged event to

 

decide what action you want to perform.

Target

If the NavigateUrl property is set, this sets the target window or frame for the

 

link. If Target isn’t set, the new page is opened in the current browser window.

 

The TreeView also exposes a Target property, which you can set to apply a

 

default target for all TreeNode instances.

ImageUrl

The image that’s displayed next to this node.

ImageToolTip

The tooltip text for the image displayed next to the node.

 

 

One unusual detail about the TreeNode is that it can be in one of two modes. In selection mode, clicking the node posts back the page and raises the TreeView.SelectedNodeChanged event. This is the default mode for all nodes. In navigation mode, clicking a node navigates to a new page, and the

586C H A P T E R 1 6 W E B S I T E N AV I G AT I O N

SelectedNodeChanged event is not raised. The TreeNode is placed in navigation mode as soon as you set the NavigateUrl property to anything other than an empty string. A TreeNode that’s bound to site map data is in navigational mode, because each site map node supplies URL information.

The next example fills a TreeView with the results of a database query. You want to use the TreeView’s ability to show hierarchical data to create a master-details list. Because ASP.NET doesn’t include any data source control that can query a database and expose the results as a hierarchical data source, you can’t use data binding. Instead, you need to programmatically query the table and create the TreeNode structure by hand.

Here’s the code that implements this approach:

protected void Page_Load(object sender, EventArgs e)

{

if (!Page.IsPostBack)

{

DataSet ds = GetProductsAndCategories();

// Loop through the category records.

foreach (DataRow row in ds.Tables["Categories"].Rows)

{

//Use the constructor that requires just text

//and a nondisplayed value.

TreeNode nodeCategory = new TreeNode( row["CategoryName"].ToString(), row["CategoryID"].ToString());

TreeView1.Nodes.Add(nodeCategory);

//Get the children (products) for this parent (category). DataRow[] childRows = row.GetChildRows(ds.Relations[0]);

//Loop through all the products in this category. foreach (DataRow childRow in childRows)

{

TreeNode nodeProduct = new TreeNode( childRow["ProductName"].ToString(), childRow["ProductID"].ToString());

nodeCategory.ChildNodes.Add(nodeProduct);

}

//Keep all categories collapsed (initially). nodeCategory.Collapse();

}

}

}

Now when a node is clicked, you can handle the SelectedNodeChanged event to show the node information:

protected void TreeView1_SelectedNodeChanged(object sender, EventArgs e)

{

if (TreeView1.SelectedNode == null) return; if (TreeView1.SelectedNode.Depth == 0)

{

lblInfo.Text = "You selected Category ID: ";

}

else if (TreeView1.SelectedNode.Depth == 1)

{

C H A P T E R 1 6 W E B S I T E N AV I G AT I O N

587

lblInfo.Text = "You selected Product ID: ";

}

lblInfo.Text += TreeView1.SelectedNode.Value;

}

Figure 16-15 shows the result.

Figure 16-15. Filling a TreeView with database data

A few options exist to simplify the page code in this example. One option is to bind to XML data instead of relational data. Seeing as SQL Server 2000 and later have the ability to perform XML queries with FOR XML, you could retrieve the data shaped in a specific XML markup and then bind it through the XmlDataSource control. The only trick is that because the XmlDataSource assumes you’ll be binding to a file, you need to set the Data property by hand with the XML extracted from the database.

Populating Nodes on Demand

If you have an extremely large amount of data to display in a TreeView, you probably don’t want to fill it in all at once. Not only will that increase the time taken to process the initial request for the page, it will also dramatically increase the size of the page and the view state. Fortunately, the TreeView includes a populate-on-demand feature that makes it easy to fill in branches of the tree as they are expanded. Even better, you can use populate-on-demand on selected portions of the tree, as you see fit.

To use populate-on-demand, you set the PopulateOnDemand property to true for any TreeNode that has content you want to fill in at the last minute. When the user expands this branch, the TreeView will fire a TreeNodePopulate event, which you can use to add the next level of nodes. If you want, this level of nodes can contain another level of nodes that are populated on demand.

Although the programming model remains fixed, the TreeView actually supports two techniques for filling in the on-demand nodes. When the TreeView.PopulateNodesFromClient property