I am building a fairly simple CMS. I need to intercept requests for the majority of .aspx pages in my web application, in order to gain complete control over the output. In most cases, the output will be pulled from cache and will just be plain HTML.
However, there still are a couple of pages that I am going to need to use asp: controls on. I assume the best way for me to bypass a few particular requests would be to inherit System.Web.UI.PageHandlerFactory and just call the MyBase implementation when I need to (please correct me if I am wrong here). But how do I transfer all other requests to my custom handler?
When I wrote a simple CMS, I had a difficult time using the PageHandlerFactory to get it to do what I wanted. In the end I switched to a IHttpModule.
My module would first check to see if there was an .aspx file in the requested path. I'd only do that if the page has user controls on it or didn't fit into the CMS for some reason. So if the file existed, it would return out of the module. After that it would look at the requested path and condense it into a "navigation tag." Thus ~/aboutus/default.aspx would become page.aspx?nt=aboutusdefault. page.aspx would load the proper content form the CMS. Of course, the redirect occurs server-side so the users/spiders never know anything different happened.
using System;
using System.Data;
using System.Collections.Generic;
using System.Configuration;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;
namespace MyCMS.Handlers {
/// <summary>
/// Checks to see if we should display a virutal page to replace the current request.
/// Code adapted from:
/// Rewrite.NET -- A URL Rewriting Engine for .NET
/// By Robert Chartier
/// http://www.15seconds.com/issue/030522.htm
/// </summary>
public class VirtualPageModule : IHttpModule {
/// <summary>
/// Init is required from the IHttpModule interface
/// </summary>
/// <param name="Appl"></param>
public void Init(System.Web.HttpApplication Appl) {
// make sure to wire up to BeginRequest
Appl.BeginRequest += new System.EventHandler(Rewrite_BeginRequest);
}
/// <summary>
/// Dispose is required from the IHttpModule interface
/// </summary>
public void Dispose() {
// make sure you clean up after yourself
}
/// <summary>
/// To handle the starting of the incoming request
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
public void Rewrite_BeginRequest(object sender, System.EventArgs args) {
// Cast the sender to an HttpApplication object
HttpApplication httpApp = (HttpApplication)sender;
// See if the requested file already exists
if (System.IO.File.Exists(httpApp.Request.PhysicalPath)) {
// Do nothing, process the request as usual
return;
}
string requestPath = VirtualPathUtility.ToAppRelative(httpApp.Request.Path);
// Organic navigation tag (~/aboutus/default.aspx = nt "aboutusdefault")
Regex regex = new Regex("[~/\\!##$%^&*()+=-]");
requestPath = regex.Replace(requestPath, string.Empty).Replace(".aspx", string.Empty);
string pageName = "~/page.aspx";
string destinationUrl = VirtualPathUtility.ToAbsolute(pageName) + "?nt=" + requestPath;
SendToNewUrl(destinationUrl, httpApp);
}
public void SendToNewUrl(string url, HttpApplication httpApp) {
applyTrailingSlashHack(httpApp);
httpApp.Context.RewritePath(
url,
false // RebaseClientPath must be false for ~/ to continue working in subdirectories.
);
}
/// <summary>
/// Applies the trailing slash hack. To circumvent an ASP.NET bug related to dynamically
/// generated virtual directories ending in a trailing slash (/).
/// As described by BuddyDvd:
/// http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=105061
/// </summary>
/// <param name="httpApp">The HttpApplication.</param>
/// <remarks>
/// Execute this function before calling RewritePath.
/// </remarks>
private void applyTrailingSlashHack(HttpApplication httpApp) {
if (httpApp.Request.Url.AbsoluteUri.EndsWith("/") && !httpApp.Request.Url.AbsolutePath.Equals("/")) {
Type requestType = httpApp.Context.Request.GetType();
object clientFilePath = requestType.InvokeMember("ClientFilePath", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.GetProperty, null, httpApp.Context.Request, null);
string virtualPathString = (string)clientFilePath.GetType().InvokeMember("_virtualPath", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.GetField, null, clientFilePath, null);
clientFilePath.GetType().InvokeMember("_virtualPath", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.SetField, null, clientFilePath, new object[] { virtualPathString });
requestType.InvokeMember("_clientFilePath", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.SetField, null, HttpContext.Current.Request, new object[] { clientFilePath });
object clientBaseDir = requestType.InvokeMember("ClientBaseDir", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.GetProperty, null, httpApp.Context.Request, null);
clientBaseDir.GetType().InvokeMember("_virtualPath", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.SetField, null, clientBaseDir, new object[] { virtualPathString });
requestType.InvokeMember("_clientBaseDir", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.SetField, null, HttpContext.Current.Request, new object[] { clientBaseDir });
}
}
}
}
Do you mean that you are going to inject controls? If that is the case, you might want to consider a required base class instead of the Page class. Page implements IHttpHandler, so you can create a derived class and then change your pages to derive from your derived class. You will have much more control over your page and be able to hook into it and its rendering.
Related
I'm working on an established site. Although small (in terms of pages), there are some big money landing pages as well as the usual stock pages.
Because the site was relatively small, the page structure was flat.
https://example.com/contact
https://example.com/big-money-page
We plan on introducing lots more pages with different page designs. This means we'll either use master pages and/or aspx templated pages and create our own database driven CMS.
Here's the problem I can see is with url routing:
Template type 1
Route url: /{Name} - e.g. /big-money-page
Physica path: ~/template1.aspx
Template type 2
Route url: /{Name} - e.g. /new-supporting-page
Physical path: ~/template2.aspx
I would like to make this work without disruption to the existing money pages and, if possible, keep the familiar website structure, as to visitors, template1 and template2 are similar pages and don't naturally reside in different folders - they just differ in design.
Also, fixed deep routed folder structures make it difficult to make changes in the future.
I've been using WF routing for some time but always in simple ways. Anyone know how I can make the changes work with limited consequences?
UPDATE --------------------------------------------------------------------
Okay, in the absence of any feedback, I've come up with an idea to put on the table. I'd appreciate feedback on the fesibility and any downsides that can be thought of.
My idea is to have a dummy route/page.
The route would take the form http://example.com/{name}.
The dummy page retrieves data from the database for the target page using the placeholder {name}.
We then server.transfer to the correct target page, using the data we retrieved from the database.
I think this will work but I'm concerned about the things I don't know:
Browser compatibility for server.transfer
Performance overhead
Impact on output caching
Other things that haven't even crossed my mind
Of course this is not an ideal solution but I'm also open to any other ideas.
In a WebForm project the task can be implemented using a custom HTTPModule. The implementation includes a few steps. A simplified version is as following:
1. SQL
create table dbo.UrlMap (
publicUrl varchar(255) not null primary key,
PhysUrl varchar(255) not null
)
Fill the table with some data like
publicUrl PhysUrl
big-money-page template1.aspx?id=1
huge-money-page template1.aspx?id=2
no-money-page template2.aspx?id=3
other-page template1.aspx?id=4
2. Create a class in the App_code folder
using System;
using System.Web;
/// <summary>
/// Implements IHttpModule with custom URLs
/// </summary>
public class UrlMap:IHttpModule
{
/// <summary>
/// Initialize the module
/// </summary>
/// <param name="context"></param>
void IHttpModule.Init(HttpApplication context)
{
context.BeginRequest += Context_BeginRequest;
context.PostMapRequestHandler += Context_PostMapRequestHandler;
}
private void Context_BeginRequest(object sender, EventArgs e)
{
var app = (HttpApplication)sender;
Url2PhysPath(app.Request.Path, app);
}
private void Context_PostMapRequestHandler(object sender, EventArgs e)
{
var app = (HttpApplication)sender;
var pg = app.Context.Handler as System.Web.UI.Page;
if (pg != null)
{
pg.PreRenderComplete += Pg_PreRenderComplete;
}
}
private void Pg_PreRenderComplete(object sender, EventArgs e)
{
ProcessPageTree((System.Web.UI.Control)sender);
}
/// <summary>
/// Replaces physical URLs on the page with "beutified" version
/// </summary>
/// <param name="control"></param>
private void ProcessPageTree(System.Web.UI.Control control)
{
var form = control as System.Web.UI.HtmlControls.HtmlForm;
if (form != null)
{
form.Action = BeautifyUrl(form.Page.Request.Url.PathAndQuery);
}
//other types in a similar way
if (control.HasControls())
{
foreach(System.Web.UI.Control c in control.Controls)
{
ProcessPageTree(c);
}
}
}
/// <summary>
/// Helper function. Can be inlined.
/// Searches "beautified" url in a DB and rewrites path
/// </summary>
/// <param name="url"></param>
/// <param name="app"></param>
private static void Url2PhysPath(string url, HttpApplication app)
{
using (var cnn = new System.Data.SqlClient.SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["SiteCnn"].ConnectionString))
{
var cmd = new System.Data.SqlClient.SqlCommand("select physPath from dbo.urlMap where publicUrl=#url", cnn);
cmd.CommandType = System.Data.CommandType.Text;
cmd.Parameters.Add("#url", System.Data.SqlDbType.VarChar, 255).Value = url;
cnn.Open();
string physPath = null;
using(var r = cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection))
{
if (r.Read())
{
physPath = (string)r["physPath"];
}
r.Close();
}
if (!string.IsNullOrEmpty(physPath))
{
app.Context.RewritePath("/" + physPath);
}
}
}
/// <summary>
/// Helper function
/// </summary>
/// <param name="physUrl"></param>
/// <returns>returns original url when nothing is found</returns>
private static string BeautifyUrl(string physUrl)
{
using (var cnn = new System.Data.SqlClient.SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["SiteCnn"].ConnectionString))
{
var cmd = new System.Data.SqlClient.SqlCommand("select publicUrl from dbo.urlMap where physPath=#url", cnn);
cmd.CommandType = System.Data.CommandType.Text;
cmd.Parameters.Add("#url", System.Data.SqlDbType.VarChar, 255).Value = physUrl;
cnn.Open();
string pubUrl = physUrl;//to return original url when nothing is found
using(var r = cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection))
{
if (r.Read())
{
pubUrl = (string)r["publicUrl"];
}
r.Close();
}
return pubUrl;
}
}
/// <summary>
/// Required by interface
/// </summary>
void IHttpModule.Dispose()
{
// throw new NotImplementedException();
}
}
3. Modify Web.config
Register the module. Add following line to configuration \ system.webServer \ modules
<add name="UrlRewriter" type="UrlMap" preCondition="managedHandler"/>
Follow up
Browser compatibility for server.transfer not a issue. Browser receives HTML only
Performance overhead Not much
Impact on output caching Cached better then template.aspx?id=123
Other things that haven't even crossed my mind Both publicUrl and physUrl must be unique. In practice you can cache direct and inverted key lookups in static Dictionary<string, string> variables.
I have several resource files, e.g.
default.aspx.resx, default.aspx.nl.resx, default.aspx.en.resx
Now when I'm on the Dutch domain the default.aspx.nl.resx is loaded.
But now I want to access the value from default.aspx.en.resx and get the English value belonging to name "title".
I can now achieve this by changing the current culture server-side, access the value and then change it back, like so:
Dim culture As CultureInfo = New CultureInfo("en")
Threading.Thread.CurrentThread.CurrentCulture = culture
Threading.Thread.CurrentThread.CurrentUICulture = culture
Dim title as String = GetLocalResourceObject("title")
culture = New CultureInfo("nl")
Threading.Thread.CurrentThread.CurrentCulture = culture
Threading.Thread.CurrentThread.CurrentUICulture = culture
But is there a better/faster way? Preferably without have to change the culture for the current thread, so I can just define which resource file I want to access and in which language?
You can add in parameter your targe culture
GetLocalResourceObject("title","YourCulture");
link : http://msdn.microsoft.com/fr-fr/library/ms149953.aspx
Edit: (Sorry I didn't know that you wanted another method different from this, but this was the only way that I managed to do:)
I managed to do this by doing:
var userLanguage = "en-US";
System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo(userLanguage);
System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo(userLanguage);
HttpContext.GetGlobalResourceObject("MyAppResource", "KeyThatIWantToGet");
Where MyAppResource is how your .resx file is named and KeyThatIWantToGet explains itself.
When not using the HttpContext (general .NET applications) I use the following helper:
/// <summary>
/// Disposable class that lets us assume a specific culture while executing
/// a certain block of code. You'd typically use it like this:
///
/// using (new CultureContext("de"))
/// {
/// // Will return the German translation of "Please click here"
/// string text = SharedResource.Please_click_here;
/// }
/// </summary>
public class CultureContext : IDisposable
{
private readonly CultureInfo _previousCulture;
private readonly CultureInfo _previousUiCulture;
public CultureContext(CultureInfo culture)
{
// Save off the previous culture (we'll restore this on disposal)
_previousCulture = Thread.CurrentThread.CurrentCulture;
_previousUiCulture = Thread.CurrentThread.CurrentUICulture;
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
}
public CultureContext(string cultureCode)
: this(new CultureInfo(cultureCode))
{
}
/// <summary>
/// Syntactic sugar so that we can switch to a culture context as follows:
///
/// using (CultureContext.For("de"))
/// {
/// // Will return the German translation of "Please click here"
/// string text = SharedResource.Please_click_here;
/// }
/// </summary>
public static CultureContext For(string cultureCode)
{
return new CultureContext(cultureCode);
}
public void Dispose()
{
// Restore the culture settings that were in place before switching
// to this context
Thread.CurrentThread.CurrentCulture = _previousCulture;
Thread.CurrentThread.CurrentUICulture = _previousUiCulture;
}
}
Using vanilla MVC I can revalidate my model with TryValidateModel. The TryValidateModel method doesn't seem to be applicable to WebAPI. How can I revalidate my model when using WebAPI?
I know it has been a while since this has been asked, but the problem is still valid. Thus i thought i should share my solution to this problem.
I decided to implement the TryValidateModel(object model) myself, based on the implementation in the System.Web.Mvc.Controller.cs
The problem is that the mvc's TryValidateModel internally used their own HttpContext and ModelState. If you go and compaire the two, they are very similar....
The be able to use our own HttpContext there exists a HttpContextWrapper that can be used for that.
And Since we have to clear our model state, it doesn't really matter that we use a different type of ModelState , as long as we get the desired result, thus i create a new ModelState object from the correct type...
I did add the error to the ModelState of the controller and not to the model state to the newly created ModelState , This seems to work just fine for me :)
Here is my code, that i just added to the controller...
do not forget to import the library...
using System.Web.ModelBinding;
protected internal bool TryValidateModel(object model)
{
return TryValidateModel(model, null /* prefix */);
}
protected internal bool TryValidateModel(object model, string prefix)
{
if (model == null)
{
throw new ArgumentNullException("model");
}
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType());
var t = new ModelBindingExecutionContext(new HttpContextWrapper(HttpContext.Current), new System.Web.ModelBinding.ModelStateDictionary());
foreach (ModelValidationResult validationResult in ModelValidator.GetModelValidator(metadata, t).Validate(null))
{
ModelState.AddModelError(validationResult.MemberName, validationResult.Message);
}
return ModelState.IsValid;
}
I don't know when was it added but now there is Validate method on api controller.
ApiController.Validate Method (TEntity)
https://msdn.microsoft.com/en-us/library/dn573258%28v=vs.118%29.aspx
Based from rik-vanmechelen original answer, here is my version that relies on the services container exposed by Web API.
/// <summary>
/// Tries to validate the model.
/// </summary>
/// <param name="model">The model.</param>
/// <returns>Whether the model is valid or not.</returns>
protected internal bool TryValidateModel(object model)
{
if (model == null)
{
throw new ArgumentNullException("model");
}
var metadataProvider = Configuration.Services.GetService<System.Web.Http.Metadata.ModelMetadataProvider>();
var validatorProviders = Configuration.Services.GetServices<System.Web.Http.Validation.ModelValidatorProvider>();
var metadata = metadataProvider.GetMetadataForType(() => model, model.GetType());
ModelState.Clear();
var modelValidators = metadata.GetValidators(validatorProviders);
foreach (var validationResult in modelValidators.SelectMany(v => v.Validate(metadata, null)))
{
ModelState.AddModelError(validationResult.MemberName, validationResult.Message);
}
return ModelState.IsValid;
}
This uses the following simple extension methods to access the services :
/// <summary>
/// Services container extension methods.
/// </summary>
public static class ServicesContainerExtensions
{
/// <summary>
/// Gets the service.
/// </summary>
/// <typeparam name="TService">The type of the service.</typeparam>
/// <param name="services">The services.</param>
/// <returns>The service.</returns>
/// <exception cref="System.ArgumentNullException">services</exception>
public static TService GetService<TService>(this ServicesContainer services)
{
if (services == null)
{
throw new ArgumentNullException("services");
}
return (TService)((object)services.GetService(typeof(TService)));
}
/// <summary>
/// Gets the services.
/// </summary>
/// <typeparam name="TService">The type of the service.</typeparam>
/// <param name="services">The services.</param>
/// <returns>The services.</returns>
/// <exception cref="System.ArgumentNullException">services</exception>
public static IEnumerable<TService> GetServices<TService>(this ServicesContainer services)
{
if (services == null)
{
throw new ArgumentNullException("services");
}
return services.GetServices(typeof(TService)).Cast<TService>();
}
}
The advantage of using this method is that it reuses the MetadataProvider and ValidatorProvider(s) you have configured for your Web API application while the previous answer is retrieving the one configured in ASP.NET MVC.
ASP.NET MVC and WebAPI run through different pipelines.
Turns out TryValidateModel is not supported in WebAPI. There's a feature request over on CodePlex.
I have an MVC3 application on .net4 that its session working in the dev Environment, but not in the production.
In the production I logged the sessionID and the it is the same in the moment I Set and Get from the session.
When I try to get the session I am getting Null Exception.
This is how I access the session:
public static class HandlersHttpStorage
{
public static string TestSession
{
get
{
return HttpContext.Current.Session["time"];//This is null
}
set
{
HttpContext.Current.Session.Add("time", value);//DateTime.Now.ToString()
}
}
}
What's makes me worried is that the behavior in the production is different than the development, even though the web.config is the same.
Solution 1:
Link: HttpContext.Current.Session is null when routing requests
Got it. Quite stupid, actually. It worked after I removed & added the SessionStateModule like so:
<configuration>
...
<system.webServer>
...
<modules>
<remove name="Session" />
<add name="Session" type="System.Web.SessionState.SessionStateModule"/>
...
</modules>
</system.webServer>
</configuration>
Simply adding it won't work since "Session" should have already been defined in the machine.config.
Now, I wonder if that is the usual thing to do. It surely doesn't seem so since it seems so crude...
Solution 2:
Link: HttpContext.Current.Session null item
sessionKey may be changing, you probably only need to do:
HttpContext.Current.Session["CurrentUser"]
Or the session may be expiring, check the timeout:
http://msdn.microsoft.com/en-us/library/h6bb9cz9(VS.71).aspx
Or you may be setting the session value from somewhere else, normally i control access to Session/Context object through one property
static readonly string SESSION_CurrentUser = "CurrentUser";
public static SiteUser Create() {
SiteUser.Current = new SiteUser();
return SiteUser.Current;
}
public static SiteUser Current {
get {
if (HttpContext.Current.Session == null || HttpContext.Current.Session[SESSION_CurrentUser] == null) {
throw new SiteUserAutorizationExeption();
}
return HttpContext.Current.Session[SESSION_CurrentUser] as SiteUser;
}
set {
if (!HttpContext.Current.Session == null) {
HttpContext.Current.Session[SESSION_CurrentUser] = value;
}
}
}
Another possible cause/solution is that IE doesn't save cookies if the domain name has an underscore (because strictly speaking domain names can't have underscores, so you'll probably only encounter this in development), e.g. http://my_dev_server/DoesntWork. Chrome or Firefox should work in this scenario, and if you change the domain name you're using to not have an underscore problem solved.
Ref:
http://blog.smartbear.com/software-quality/internet-explorer-eats-cookies-with-underscores-in-the-hostname/
http://social.msdn.microsoft.com/Forums/ie/en-US/8e876e9e-b223-4f84-a5d1-1eda2c2bbdf4/ie7-cookie-issue-when-domain-name-has-underscore-character-in-it?forum=iewebdevelopment
For me, I found that HttpContext.Current was null, so I created it:
System.Web.HttpContext c = System.Web.HttpContext.Current;
And I passed that into my function that was in my other class, like this:
string myString = "Something to save";
SessionExtensions.SetDataToSession<string>(c, "MyKey1", myString);
I had actually wanted my function to be a real extension method off of Session like the one below, but what I found was this HttpSessionStateBase session was null, it would give the NullReferenceException when I tried to add anything to Session using it. So this:
public static class SessionExtensions
{
/// <summary>
/// Get value.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="session"></param>
/// <param name="key"></param>
/// <returns></returns>
public static T GetDataFromSession<T>(this HttpSessionStateBase session, string key)
{
return (T)session[key];
}
/// <summary>
/// Set value.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="session"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static void SetDataToSession<T>(this HttpSessionStateBase session, string key, object value)
{
session[key] = value;
}
}
That Microsoft had here: https://code.msdn.microsoft.com/How-to-create-and-access-447ada98 became this, instead:
public static class SessionExtensions
{
/// <summary>
/// Get value.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="session"></param>
/// <param name="key"></param>
/// <returns></returns>
public static T GetDataFromSession<T>(HttpContext context, string key)
{
if (context != null && context.Session != null)
{
context.Session.Abandon();
}
return (T)context.Session[key];
}
/// <summary>
/// Set value.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="session"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static void SetDataToSession<T>(HttpContext context, string key, object value)
{
context.Session[key] = value;
}
}
And I was able to retrieve my data like this:
System.Web.HttpContext c = System.Web.HttpContext.Current;
string myString = SessionExtensions.GetDataFromSession<string>(c, "MyKey1");
And, of course, since HttpContext.Current and Session now exists, I was able to even simplify that to be:
string myString = Session["MyKey1"].ToString();
If this had been object, you would put the object's type in place of <string> in the SetDataToSession()
function:
List<string> myStringList = new List<string>();
myStringList.Add("Something to save");
SessionExtensions.SetDataToSession<List<string>>(c, "MyKey1", myStringList);
And to retrieve it:
System.Web.HttpContext c = System.Web.HttpContext.Current;
List<string> myStringList = SessionExtensions.GetDataFromSession<List<string>>(c, "MyKey1");
or simply:
List<string> myStringList = (List<string>)Session["MyKey1"];
How do I access the Control (dropdownlist) in current Page from the UserControl?
In the UserControl:
String test = ((DropDownList)this.Parent.FindControl("drpdwnlstMainRegion")).SelectedValue;
or
String test = ((DropDownList)this.Page.FindControl("drpdwnlstMainRegion")).SelectedValue;
It return null on ((DropDownList)this.Parent.FindControl("drpdwnlstMainRegion")) for some reason?!?!
BTW ... I am using ASP.NET C# 3.5.
Thanks
Depending upon the structure of your page and the nesting of the controls, you may have to recursively crawl through all of the controls. Something like the following may be helpful: http://stevesmithblog.com/blog/recursive-findcontrol/
Compile these extension methods into your assembly:
using System.Collections.Generic;
using System.Linq;
using System.Web.UI;
public static class ControlExtensions
{
/// <summary>
/// Recurses through a control tree and returns an IEnumerable<Control>
/// containing all Controls from the control tree
/// </summary>
/// <returns>an IEnumerable<Control></returns>
public static IEnumerable<Control> FindAllControls(this Control control)
{
yield return control;
foreach (Control child in control.Controls)
foreach (Control all in child.FindAllControls())
yield return all;
}
/// <summary>
/// Recurses through a control tree and finds a control with
/// the ID specified
/// </summary>
/// <param name="control">The current object</param>
/// <param name="id">The ID of the control to locate</param>
/// <returns>A control of null if more than one control is found with a matching ID</returns>
public static Control FindControlRecursive(this Control control, string id)
{
var controls = from c in control.FindAllControls()
where c.ID == id
select c;
if (controls.Count() == 1)
return controls.First();
return null;
}
}
And then use like this:
Control whatYoureLookingFor = Page.Master.FindControlRecursive("theIdYouAreLookingFor");
This is a duplicate of a couple of questions already on SO but I couldn't find them.