I would like to create an custom server control, VersionedContentControl, which will allow me to specify different variations of the final markup.
Example usage:
<custom:VersionedContentControl runat="server" VersionToUse="2">
<ContentVersions>
<Content Version="1">
<asp:HyperLink runat="server" ID="HomeLink" NavigateUrl="~/Default.aspx">Home</asp:HyperLink>
</Content>
<Content Version="2">
<asp:LinkButton runat="server" ID="HomeLink" OnClick="GoHome">Home</asp:LinkButton>
</Content>
<Content Version="3">
<custom:HomeLink runat="server" ID="HomeLink" />
</Content>
</ContentVersions>
</custom:VersionedContentControl>
Using the markup above, I would expect the only LinkButton control to be utilized on the page.
Long Story
I am having great difficulty trying to define this custom control. I haven't even been able to find a good example on the MSDN of using nested controls like this. Instead, I have had to resort to following these blog posts as examples:
http://blog.spontaneouspublicity.com/child-collections-in-asp-net-custom-controls
http://www.tomot.de/en-us/article/2/asp.net/how-to-create-an-asp.net-control-that-behaves-as-a-template-container-to-nest-content-via-markup
Unfortunately, everything I have tried has failed miserably. Here is what I have currently:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.UI;
namespace CustomControls
{
[ParseChildren(true)]
[PersistChildren(false)]
public class VersionedContentControl : Control, INamingContainer
{
public string VersionToUse { get; set; }
[PersistenceMode(PersistenceMode.InnerProperty)]
public IList<Content> ContentVersions { get; set; }
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
var controlToUse = ContentVersions.Single(x => x.Version == VersionToUse);
Controls.Clear();
controlToUse.InstantiateIn(this);
}
}
public class Content : ITemplate
{
public string Version { get; set; }
public void InstantiateIn(Control container)
{
// I don't know what this method should do
}
}
public class ContentVersionsList : List<Content> {}
}
Even though I haven't implemented InstantiateIn, all 3 versions of my content appear on the page; it shows 3 links.
Also, I can't actually use the control unless I specify different ID property values for each nested control; I can't use "HomeLink" for all of them. I would like to be able to re-use the ID so that I can access the control from the code behind.
I realize that, normally, it is forbidden to specify duplicate ID values for multiple controls on a page. However, in the MSDN documentation for System.Web.UI.MobileControls.DeviceSpecific, the examples use duplicate ID values for nested controls. Infact, the example is very close to what I want to do; it differs content based on a mobile device compatibility filter.
<mobile:Form id="Form1" runat="server">
<mobile:DeviceSpecific Runat="server">
<Choice Filter="isHTML32">
<HeaderTemplate>
<mobile:Label ID="Label1" Runat="server">
Header Template - HTML32</mobile:Label>
<mobile:Command Runat="server">
Submit</mobile:Command>
</HeaderTemplate>
<FooterTemplate>
<mobile:Label ID="Label2" Runat="server">
Footer Template</mobile:Label>
</FooterTemplate>
</Choice>
<Choice>
<HeaderTemplate>
<mobile:Label ID="Label1" Runat="server">
Header Template - Default</mobile:Label>
<mobile:Command ID="Command1" Runat="server">
Submit</mobile:Command>
</HeaderTemplate>
<FooterTemplate>
<mobile:Label ID="Label2" Runat="server">
Footer Template</mobile:Label>
</FooterTemplate>
</Choice>
</mobile:DeviceSpecific>
</mobile:Form>
It would be nice to look at the source of those controls to see how they accomplish this but, unfortunately, it is not open-source.
My Question
How can I create a custom server control which contains a list of nested controls and only renders one of the nested controls based on a property? Ideally re-using IDs among separate nested controls.
tl;dr
I ended up using the MultiView control. It doesn't allow duplicate IDs but it satisfies all my other requirements and it allows me avoid having to maintain any custom code.
What I tried
I was finally able to get something working with this code:
using System.Collections.Generic;
using System.Linq;
using System.Web.UI;
namespace CustomControls
{
[ParseChildren(true)]
[PersistChildren(false)]
public class VersionedContent : Control
{
public string VersionToUse { get; set; }
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(ContentContainer))]
[TemplateInstance(TemplateInstance.Multiple)]
public List<Content> ContentVersions { get; set; }
public override ControlCollection Controls
{
get
{
EnsureChildControls();
return base.Controls;
}
}
public ContentContainer ContentContainer
{
get
{
EnsureChildControls();
return _contentContainer;
}
} private ContentContainer _contentContainer;
protected override void CreateChildControls()
{
var controlToUse = ContentVersions.Single(x => x.Version == VersionToUse);
Controls.Clear();
_contentContainer = new ContentContainer();
controlToUse.InstantiateIn(_contentContainer);
Controls.Add(_contentContainer);
}
}
public class Content : Control, ITemplate
{
public string Version { get; set; }
public void InstantiateIn(Control container)
{
container.Controls.Add(this);
}
}
public class ContentContainer : Control { }
}
This allows me to use the control like so:
<custom:VersionedContent ID="VersionedContentControl" runat="server" VersionToUse="1">
<ContentVersions>
<custom:Content Version="1">
<custom:MyControlV1 runat="server" />
</custom:Content>
<custom:Content Version="2">
<custom:MyControlV2 runat="server" CustomProperty="Foo" />
</custom:Content>
</ContentVersions>
</custom:VersionedContent>
Unfortunately, this solution had several drawbacks...
Even though I was using Template instances, I wasn't allowed to use duplicate IDs in separate Content sections
ViewState was not being handled properly
PostBack doesn't work properly
Validation doesn't work at all
After looking at the decompiled source code of the MultiView control (which is similar), I realized I would have had to make the code a lot more complicated to make it work as desired. The only thing I would gain over just using the MultiView control was possibly being able to use duplicate IDs, if I could even get that working. I decided it would be best to just settle for using the build-in MultiView control.
protected override void RenderContents(HtmlTextWriter output)
{
output.Write("<div><div class=\"UserSectionHead\">");
Label l = new Label() { Text = Label };
TextBox t = new TextBox() { Text = Text };
l.AssociatedControlID = t.ID;
l.RenderControl(output);
output.Write("</div><div class=\"UserSectionBody\"><div class=\"UserControlGroup\"><nobr>");
t.RenderControl(output);
output.Write("</nobr></div></div><div style=\"width:100%\" class=\"UserDottedLine\"></div>");
}
Related
How do we bind an enum value to the property of a web user control? I keep receiving this error.
Cannot create an object of type 'MyEnum' from its string representation 'MyEnum.MyValue' for the 'MyProperty' property.
This is the code behind MyWebUserControl.ascx.cs
[System.ComponentModel.Bindable(true)]
public MyEnum MyProperty { get; set; }
SomeWebUserControl.ascx
<!-- This fails -->
<uc1:MyWebUserControl runat="server" ID="MyControl1"
MyProperty="MyEnum.MyValue" />
<!-- This fails -->
<uc1:MyWebUserControl runat="server" ID="MyControl2"
MyProperty="<%# MyEnum.MyValue %>" />
<!-- This works -->
<uc1:MyWebUserControl runat="server" ID="MyControl3"
MyProperty="0" />
Defining the property on the control is easy, as you have done:
[System.ComponentModel.Bindable(true)]
public MyEnum MyProperty { get; set; }
However, it's worth noting that the [Bindable(true)] attribute isn't needed. It's just there as a hint for VS to offer additional databinding properties in the property grid. It doesn't affect the ASP.NET runtime at all.
To set this property statically in the ASPX markup, just use the enum's value:
<uc1:MyWebUserControl runat="server"
ID="MyControl1"
MyProperty="MyValue" />
If you want to set it dynamically, you can do it in Page_Init:
void Page_Init() {
MyControl1.MyProperty = GetEnumValueFromSomewhere();
}
<uc1:MyWebUserControl runat="server"
ID="MyControl1" />
And finally, to set it dynamically as part of databinding, you can use the <%# foo %> databinding syntax:
<uc1:MyWebUserControl runat="server"
ID="MyControl1"
MyProperty="<%# GetEnumValueFromSomewhere() %>" />
Note that the databinding option should be used only when databinding. If you're not trying to do databinding, it is rather wasteful to call DataBind() on the entire page, or even just on that one control.
This workaround might help solve the issue. You can tie the "real" property to a string property like so:
private MyEnum _MyProperty = MyEnum.Default;
[System.ComponentModel.Bindable(true)]
public MyEnum MyProperty
{
get {
return _MyProperty;
}
set {
_MyProperty = value;
}
}
public string MyPropertyString
{
get {
return _MyProperty.ToString();
}
set {
_MyProperty = (MyEnum)Enum.Parse( typeof(MyEnum), value, true );
}
}
And use 'MyPropertyString` on the control. It's not an elegant solution, but it may do the trick. Seems like there HAS to be a better answer than this, but I looked around and was not able to find anything yet.
Page.DataBind()
Page.DataBind() is necessary - that's what was missing.
[System.ComponentModel.Bindable(true)] is NOT necessary on the property.
Example
MyWebUserControl.ascx.cs
public MyEnum MyProperty { get; set; }
SomeWebUserControl.ascx
<uc1:MyWebUserControl runat="server" ID="MyControl"
MyProperty="<%# MyEnum.MyValue %>" />
SomeWebUserControl.ascx.cs
protected void Page_Load(object sender, EventArgs e)
{
// this is 100% necessary to bind enum
Page.DataBind();
}
See also: Render DateTime.Now directly in the ASPX page
I can see from other people's controls that it is possible to set sub-object properites in markup. For example, if I'm using Telerik's RadComboBox I can write ...
<telerik:RadComboBox runat="server" ID="RadComboBox2">
<CollapseAnimation Duration="100" />
</telerik:RadComboBox>
or, alternatively I can write...
<telerik:RadComboBox runat="server" ID="RadComboBox2" CollapseAnimation-Duration="100">
</telerik:RadComboBox>
What technique do I have to emply to allow me to do this with controls I write? I thought that I might have to explicitly create properties in my parent control for each of the of properties my sub-object I expose. However, I don't seem to allowed to create a property with a '-' in the name.
Try this:
1 - Property Class Definition
public class Option
{
public string First { get; set; }
public string Last { get; set; }
}
2 - UserControl Definition
public partial class CustomUC : System.Web.UI.UserControl
{
//Enables the Option properties to be filled inside the control's tag
[PersistenceMode(PersistenceMode.InnerProperty)]
//Enables the Option properties to be filled on the control's tag
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Option Options
{
get;set;
}
protected void Page_Load(object sender, EventArgs e) { }
}
3 - Markup:
<own:CustomUC ID="uc" runat="server" Options-First="First" Options-Last="Last" />
or
<own:CustomUC ID="uc" runat="server" >
<Options-First="First" Options-Last="Last" />
</own:CustomUC>
Note: You have to reference the usercontrol assembly first with your own tagPrefix.
I'm creating a custom server control in ASP.NET WebForms and want to have a hyphen in my property name the same way the ASP.NET server controls do in the markup. For example, a Label control has a "Font-Size" property in the markup like so:
<asp:Label ID="Label1" Font-Size="small" Text="hi" runat="server" />
How do I accomplish this?
Just use complex properties on your control:
public class MyControl: WebControl
{
public Test()
{
// Make sure to initialize the complex property or you will get a NRE
// when you try to set the Complex-Bar property in the webpage
Complex = new Complex();
}
public Complex Complex { get; set; }
}
public class Complex
{
public string Bar { get; set; }
}
and then:
<asp:MyControl runat="server" ID="myControl" Complex-Bar="foo bar" />
I added the following to get intellisense working with the complex property:
[Category("Appearance")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[PersistenceMode(PersistenceMode.InnerProperty)]
I have inherited an asp.net web app , that passes information to a gridview. Each row has different buttons and links on it and they are enabled/disabled based on the information received. There is a very long chain of logic that each row goes through to set up the buttons, which makes it very difficult to read. Is there a way to set up a class of buttons to make this easier to read?
Not sure what you mean by a "class of buttons", but you can create a custom server controls as a wrapper to the button control, and provide some extra properties or methods to simplify the GridView logic.
Here's a quick and dirty example of a RadioButton wrapper that we built, which extends the properties of the standard Radio Button so it can hold extra information. You can try doing something similar for your GridView buttons:
[DefaultProperty("Text")]
[ToolboxData("<{0}:RadioButton runat=server></{0}:RadioButton>")]
public class RadioButton : System.Web.UI.WebControls.RadioButton
{
[Bindable(true)]
[DefaultValue("")]
[Localizable(true)]
public string Value
{
get
{
string RadioValue = (string)ViewState["Value"];
return (RadioValue == null) ? String.Empty : RadioValue;
}
set
{
ViewState["Value"] = value;
}
}
protected override void RenderContents(HtmlTextWriter output)
{
output.Write(Text);
}
}
One thing that you could do quickly would be to convert the DataSource to a report class. You could then move the logic for enabling/disabling into that class (i.e., individual property per button), and update the grid to simply check the appropriate property per link/button.
This at least moves the logic out of the aspx file to somewhere it can be maintained/perused more easily.
For example:
grid.DataSource = MyReportClass.GetReport();
Where
public MyReportClass
{
public string Name { get; set; }
public string EnableLink1 { get { //logic here } }
public string EnableButton3 { get { ///logic here } }
public static List<MyReportClass> GetReport()
{
// get the data
}
}
then the aspx becomes
<gridview id="grid" runat="server" ... >
...
<asp:templatefield headertext="Link1" ><itemtemplate>
<asp:linkbutton id="l1" runat="server" ...
visible='<%# !(bool)DataBinder.Eval(Container.DataItem, "EnableLink1") %>'
/>
</itemTemplate></asp:templatefield>
...
</gridview>
I'm just getting started with Custom User Controls in C# and I'm wondering if there are any examples out there of how to write one which accepts nested tags?
For example, when you create an asp:repeater you can add a nested tag for itemtemplate.
I wrote a blog post about this some time ago. In brief, if you had a control with the following markup:
<Abc:CustomControlUno runat="server" ID="Control1">
<Children>
<Abc:Control1Child IntegerProperty="1" />
</Children>
</Abc:CustomControlUno>
You'd need the code in the control to be along the lines of:
[ParseChildren(true)]
[PersistChildren(true)]
[ToolboxData("<{0}:CustomControlUno runat=server></{0}:CustomControlUno>")]
public class CustomControlUno : WebControl, INamingContainer
{
private Control1ChildrenCollection _children;
[PersistenceMode(PersistenceMode.InnerProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Control1ChildrenCollection Children
{
get
{
if (_children == null)
{
_children = new Control1ChildrenCollection();
}
return _children;
}
}
}
public class Control1ChildrenCollection : List<Control1Child>
{
}
public class Control1Child
{
public int IntegerProperty { get; set; }
}
I followed Rob's blog post, and made a slightly different control. The control is a conditional one, really just like an if-clause:
<wc:PriceInfo runat="server" ID="PriceInfo">
<IfDiscount>
You don't have a discount.
</IfDiscount>
<IfNotDiscount>
Lucky you, <b>you have a discount!</b>
</IfNotDiscount>
</wc:PriceInfo>
In the code I then set the HasDiscount property of the control to a boolean, which decides which clause is rendered.
The big difference from Rob's solution, is that the clauses within the control really can hold arbitrary HTML/ASPX code.
And here is the code for the control:
using System.ComponentModel;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace WebUtilities
{
[ToolboxData("<{0}:PriceInfo runat=server></{0}:PriceInfo>")]
public class PriceInfo : WebControl, INamingContainer
{
private readonly Control ifDiscountControl = new Control();
private readonly Control ifNotDiscountControl = new Control();
public bool HasDiscount { get; set; }
[PersistenceMode(PersistenceMode.InnerProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Control IfDiscount
{
get { return ifDiscountControl; }
}
[PersistenceMode(PersistenceMode.InnerProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Control IfNotDiscount
{
get { return ifNotDiscountControl; }
}
public override void RenderControl(HtmlTextWriter writer)
{
if (HasDiscount)
ifDiscountControl.RenderControl(writer);
else
ifNotDiscountControl.RenderControl(writer);
}
}
}
I ended up with something very similar to the answer by Rob (in wayback archive) #gudmundur-h, but I used ITemplate to get rid of that annoying "You can't place content between X tags" in the usage. I'm not entirely sure what is actually required or not, so it's all here just in case.
The partial/user control markup: mycontrol.ascx
Note the important bits: plcChild1 and plcChild2.
<!-- markup, controls, etc -->
<div class="shell">
<!-- etc -->
<!-- optional content with default, will map to `ChildContentOne` -->
<asp:PlaceHolder ID="plcChild1" runat="server">
Some default content in the first child.
Will show this unless overwritten.
Include HTML, controls, whatever.
</asp:PlaceHolder>
<!-- etc -->
<!-- optional content, no default, will map to `ChildContentTwo` -->
<asp:PlaceHolder ID="plcChild2" runat="server"></asp:PlaceHolder>
</div>
The partial/user control codebehind: mycontrol.ascx.cs
[ParseChildren(true), PersistChildren(true)]
[ToolboxData(false /* don't care about drag-n-drop */)]
public partial class MyControlWithNestedContent: System.Web.UI.UserControl, INamingContainer {
// expose properties as attributes, etc
/// <summary>
/// "attach" template to child controls
/// </summary>
/// <param name="template">the exposed markup "property"</param>
/// <param name="control">the actual rendered control</param>
protected virtual void attachContent(ITemplate template, Control control) {
if(null != template) template.InstantiateIn(control);
}
[PersistenceMode(PersistenceMode.InnerProperty),
DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public virtual ITemplate ChildContentOne { get; set; }
[PersistenceMode(PersistenceMode.InnerProperty), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public virtual ITemplate ChildContentTwo { get; set; }
protected override void CreateChildControls() {
// clear stuff, other setup, etc
// needed?
base.CreateChildControls();
this.EnsureChildControls(); // cuz...we want them?
// using the templates, set up the appropriate child controls
attachContent(this.ChildContentOne, this.plcChild1);
attachContent(this.ChildContentTwo, this.plcChild2);
}
}
Important bits (?):
ParseChildren -- so stuff shows up?
PersistChildren -- so dynamically created stuff doesn't get reset?
PersistenceMode(PersistenceMode.InnerProperty) -- so controls are parsed correctly
DesignerSerializationVisibility(DesignerSerializationVisibility.Content) -- ditto?
The control usage
<%# Register Src="~/App_Controls/MyStuff/mycontrol.ascx" TagPrefix="me" TagName="MyNestedControl" %>
<me:MyNestedControl SomeProperty="foo" SomethingElse="bar" runat="server" ID="meWhatever">
<%-- omit `ChildContentOne` to use default --%>
<ChildContentTwo>Stuff at the bottom! (not empty anymore)</ChildContentTwo>
</me:MyNestedControl>
My guess is you're looking for something like this? http://msdn.microsoft.com/en-us/library/aa478964.aspx
Your tags were removed or are invisible, so can't really help you there.