ASP.NET 4.0 allows CON, PRN, COM, LPT and NUL in the URLs
On a separate note, ASP.Net 4.0 fixed this issue with a web.config setting.
A while ago I was writing about the annoying The Controls collection cannot be modified because the control contains code blocks (i.e. <% ... %>). error and how to fix it by using a PlaceHolder or another container to hold your code blocks you can resolve this.
But what if the ASPX code is not yours and you are only building the control? How can you get around the dreaded code blocks? First, let's try to understand the mechanism that renders the code blocks. When the markup code is read, a CodeDomTreeGenerator class is used to parse it. All DOM tree generators inherit from BaseTemplateCodeDomTreeGenerator which does the following: if the block read is a control, create the control and add it to the control collection, if it is a code block, generate a dynamic render method and use that. From that moment on, you can't change the control collection because (stupid, if you ask me) the render method has already been generated and it only knows about the controls it had then.
You can test if the ControlCollection object cannot be changed via the IsReadOnly property. If it is read only, code blocks have been added. Indeed, in the ControlCollection class, a private field is used to hold an error message and, if not null, it will be used as the exception message when trying to add or remove controls from the collection.
Are you in the mood for some insanity? Ok, let's unset the string via reflection, see what happens! Well, first of all, no error! You can manipulate the control collection at your leisure. The problem is that the render method is still the generated one. If you change the control collection weird stuff will happen, like not getting your control rendered or, if inserted or deleted, seeing controls rendered instead of others or being pushed out of the "rendering queue". So, what is we remove the render method as well? Then the normal Render mechanism will be used. That means that the code blocks will be completely ignored!
So, if you are a mean son of a bitch like myself, instead of begging junior programmers to never use code blocks or to encapsulate them at least, screw the controls so that they ignore that bad code. Not very smart, but oh, so mean :)
Here is a bit of code to remove the "readonlyness" of control collections:
public static class MSOAB
{
private static readonly FieldInfo _readOnlyErrorMsgFieldInfo =
typeof(ControlCollection).GetField("_readOnlyErrorMsg",
BindingFlags.Instance | BindingFlags.NonPublic);
private static readonly PropertyInfo _rareFieldsEnsuredPropertyInfo =
typeof(Control).GetProperty("RareFieldsEnsured",
BindingFlags.Instance | BindingFlags.NonPublic);
private static FieldInfo _renderMethodFieldInfo;
public static void FixReadOnlyControlCollection(Control control)
{
if (control.Controls.IsReadOnly)
{
_readOnlyErrorMsgFieldInfo.SetValue(control.Controls, null);
var rareFieldsEnsured = _rareFieldsEnsuredPropertyInfo.GetValue(control, new object[] { });
if (_renderMethodFieldInfo == null)
{
_renderMethodFieldInfo = rareFieldsEnsured.GetType().GetField("RenderMethod");
}
_renderMethodFieldInfo.SetValue(rareFieldsEnsured, null);
}
}
}
This isn't really tested except the basic functionality and I haven't used it in a production environment, but it was fun as it was. I hope you enjoyed it as well.
private void fixAutoPostBack(TextBox tb)
{
if (!tb.AutoPostBack)
return;
tb.AutoPostBack = false;
PostBackOptions options
= new PostBackOptions(tb, string.Empty)
{
TrackFocus = true,
AutoPostBack = true
};
if (tb.CausesValidation)
{
options.PerformValidation = true;
options.ValidationGroup = tb.ValidationGroup;
}
string onchange = string.Empty;
if (tb.HasAttributes)
{
onchange = tb.Attributes["onchange"];
if (!string.IsNullOrEmpty(onchange))
{
onchange = onchange.TrimEnd(';') + ";";
}
}
onchange += ClientScript
.GetPostBackEventReference(options, true);
tb.Attributes["onchange"] = onchange;
}
Now go into the designer and modify the Text property of InnerTag. You will notice that it will be saved as a hyphen attribute, while adding items to the Items collection would render the list items as children in the BaseClass tag:
public class BaseClass:ListControl
{
private InnerTagClass mInnerTag;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[NotifyParentProperty(true)]
public virtual InnerTagClass InnerTag
{
get
{
if (mInnerTag==null)
{
mInnerTag=new InnerTagClass();
}
return mInnerTag;
}
set
{
mInnerTag = value;
}
}
}
Now, an InnerDefaultProperty is still an InnerProperty. You can put the ListItem inside an Items tag and it will still work, and also you can add an InnerTag tag with a Text attribute and it will work as well! However, if you go into the designer and you change the Text for the InnerTag property you get the changed property as an attribute and the previous InnerTag tag, with the old value. That is not acceptable.
<local:BaseClass ID="BaseClass1" runat=server InnerTag-Text="Test">
<asp:ListItem>Test</asp:ListItem>
</local:BaseClass>
Now editing in the designer will create the Items tag and add the list items to it and an InnerTag tag with a Text attribute! But when you type it in source view, you get intellisense for both the Items tag and the ListItem tag! Adding only list items to the control works just like an inner default property.
public class InheritedClass : BaseClass
{
[PersistenceMode(PersistenceMode.InnerProperty)]
public override System.Web.UI.WebControls.ListItemCollection Items
{
get
{
return base.Items;
}
}
[PersistenceMode(PersistenceMode.InnerProperty)]
public override InnerTagClass InnerTag
{
get
{
return base.InnerTag;
}
set
{
base.InnerTag = value;
}
}
}
var sessionID=(string)Request.QueryString["SessionIdentifier"];
if (Request.Cookies["ASP.NET_SessionId"] == null
&& sessionID != null)
{
Request.Cookies.Add(new HttpCookie("ASP.NET_SessionId", sessionID);
}
protected override void Render(HtmlTextWriter writer)
{
if (DesignMode)
{
writer.AddStyleAttribute("float", "left");
writer.RenderBeginTag("div");
// this renders an embedded CSS file as a style block
writer.Write(this.RenderDesignTimeCss("MyButton.css"));
}
base.Render(writer);
if (DesignMode)
{
writer.RenderEndTag();
}
}
private static readonly Regex sRegexWebResource =
new Regex(@"\<%\s*=\s*WebResource\(""(?<resourceName>.+?)""\)\s*%\>",
RegexOptions.ExplicitCapture | RegexOptions.Compiled);
public static string RenderDesignTimeCss2(Control control, string cssResource)
{
var type = control.GetType();
/*or a type in the assembly containing the CSS file*/
Assembly executingAssembly = type.Assembly;
var stream=executingAssembly.GetManifestResourceStream(cssResource);
string content;
using (stream)
{
StreamReader reader = new StreamReader(stream);
using (reader)
{
content = reader.ReadToEnd();
}
}
content = sRegexWebResource
.Replace(content,
new MatchEvaluator(m =>
{
return
control.GetWebResourceUrl(
m.Groups["resourceName"].Value);
}));
return string.Format("<style>{0}</style>", content);
}
if (DesignMode) {
writer.Write(
RenderDesignTimeCss2(
this,"MyNamespace.Controls.Resources.MyControl.css"
);
}
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Web.UI;
using System.Web.UI.Design;
using System.Web.UI.WebControls;
namespace Web.Controls
{
[Designer(typeof (TestControlDesigner), typeof (IDesigner))]
[ParseChildren(true)]
[PersistChildren(false)]
public class TestControl : WebControl
{
#region Properties
[EditorBrowsable(EditorBrowsableState.Never)]
[Browsable(false)]
[PersistenceMode(PersistenceMode.InnerProperty)]
[DefaultValue(typeof (ITemplate), "")]
[TemplateInstance(TemplateInstance.Single)]
public ITemplate MainTemplate
{
get;
set;
}
#endregion
#region Private Methods
protected override void CreateChildControls()
{
base.CreateChildControls();
if (MainTemplate != null)
{
MainTemplate.InstantiateIn(this);
}
}
#endregion
}
public class TestControlDesigner : ControlDesigner
{
#region Member data
private TestControl mTestControl;
#endregion
#region Public Methods
public override void Initialize(IComponent component)
{
base.Initialize(component);
SetViewFlags(ViewFlags.TemplateEditing, true);
mTestControl = (TestControl) component;
}
public override string GetDesignTimeHtml()
{
mTestControl.Attributes[DesignerRegion.DesignerRegionAttributeName] = "0";
return base.GetDesignTimeHtml();
}
public override string GetDesignTimeHtml(DesignerRegionCollection regions)
{
var region = new EditableDesignerRegion(this, "MainTemplate");
region.Properties[typeof (Control)] = mTestControl;
region.EnsureSize = true;
regions.Add(region);
string html = GetDesignTimeHtml();
return html;
}
public override string GetEditableDesignerRegionContent(
EditableDesignerRegion region)
{
if (region.Name == "MainTemplate")
{
var service = (IDesignerHost) Component.Site.GetService(typeof (IDesignerHost));
if (service != null)
{
ITemplate template = mTestControl.MainTemplate;
if (template != null)
{
return ControlPersister.PersistTemplate(template, service);
}
}
}
return string.Empty;
}
public override void SetEditableDesignerRegionContent(
EditableDesignerRegion region,string content)
{
if (region.Name == "MainTemplate")
{
if (string.IsNullOrEmpty(content))
{
mTestControl.MainTemplate = null;
}
else
{
var service = (IDesignerHost)
Component.Site.GetService(typeof (IDesignerHost));
if (service != null)
{
ITemplate template = ControlParser.ParseTemplate(service, content);
if (template != null)
{
mTestControl.MainTemplate = template;
}
}
}
}
}
#endregion
#region Properties
public override TemplateGroupCollection TemplateGroups
{
get
{
var collection = new TemplateGroupCollection();
var group = new TemplateGroup("MainTemplate");
var template = new TemplateDefinition(this,
"MainTemplate", mTestControl, "MainTemplate", true);
group.AddTemplateDefinition(template);
collection.Add(group);
return collection;
}
}
#endregion
}
}
The thing to do is use .Net 2.0 web parts and override CreateEditorParts or implement IWebEditable on generic controls that are not web parts.
A good link on how to do that (once you know what to look for, duh!) is How to get EditorParts working in your WebParts.
Another link that summarises different solutions for several issues with web parts is WSS 3.0 webparts development. A quote that is relevant to this blog entry:
Toolpanes can be customized in a number of ways. A web part by itself can declare custom toolparts which show up in the toolpane by overriding the GetToolParts method (WSS WebPart) or CreateEditorParts (ASP.NET WebPart).
A developer can also customize the toolpane generically by supporting the ICustomizeToolpane interface (which allows you to redefine the structure of the toolpane UI) or supporting IAddToGallery (which allows you to add new gallery "tabs").
I am still working on clarifying that out as the documentation from MSDN is almost non existent on the subject. It appears that using ICustomizeToolPane is only supported for Sharepoint web parts and not for ASP.Net web parts. I yet know of no other way of changing the toolpane except by using some javascript from the web part editor (which would suck) and it might not even be possible.
Update: The rest of this post is not longer relevant. I believe it is the way it was done in the old Sharepoint days, when Sharepoint web parts were not based on ASP.Net 2.0.
Notice anything? There is no mention of IValidator, instead only BaseValidators are sought and the reason the IValidator approach works at all is the line before last where a validator is added to the list if the validationGroup parameter is empty. That is, it will work with IValidators but only on Buttons or other postback controls that have an empty ValidationGroup.
public ValidatorCollection GetValidators(string validationGroup)
{
if (validationGroup == null)
{
validationGroup = string.Empty;
}
ValidatorCollection validators = new ValidatorCollection();
if (this._validators != null)
{
for (int i = 0; i < this.Validators.Count; i++)
{
BaseValidator validator = this.Validators[i] as BaseValidator;
if (validator != null)
{
if (string.Compare(validator.ValidationGroup,
validationGroup, StringComparison.Ordinal) == 0)
{
validators.Add(validator);
}
}
else if (validationGroup.Length == 0)
{
validators.Add(this.Validators[i]);
}
}
}
return validators;
}