Pass parameter into UserControl from Master page - c#

Bear with me, first time with UserControls and this is a big application I'm inheriting...
I have a user control in a master page. That works. I need to reuse about 99% of it barring one conditional css class. My thought was to key off the ID and return the correct class.
.ascx:
<div class="<%= GetMyClass() %>">Changes colors!</div>
.ascx.cx:
public string GetMyClass(){
if (ID == "FirstId"){
return "first-class";
}
if (ID == "SecondId"){
return "second-class";
}
return "";
}
.master:
<slc:MyUsercontrol runat="server" ID="FirstId" />
... stuff
<slc:MyUsercontrol runat="server" ID="SecondId" />
Two things happened here.
First, with a breakpoint in my codebehind for the user control, it only breaks once, where I would expect it to break twice.
Second, the ID available in the codebehind is not the "ID" passed in to the page. If I change "FirstId" to "Slamalamadingdong" the value is still "FirstId" in the codebehind when it hits my breakpoint.
Note that "FirstId" was set when I inherited the project, I added the markup with "SecondId", so I'm supposing that this value is actually set up somewhere else completely and the markup value is irrelevant when we get to the codebehind.
Am I completely off base in how I'm trying to pull this off?

To pass a parameter into the UserControl, instead of relying on the ID expose a new property for the UserControl as so:
.ascx.cx:
public string MyClass { get; set; }
.ascx:
<div class="<%= MyClass %>">Changes colors!</div>
.master:
<slc:MyUsercontrol runat="server" ID="FirstId" MyClass="first-class" />
<slc:MyUsercontrol runat="server" ID="SecondId" MyClass="second-class" />
This is a much more generic approach since you can simply add other MyUsercontrol elements with their own behavior without changing MyUsercontrol's logic. Also coupling the UserControl logic with the ID from his parent is not a good idea.

Related

Custom Control with Standard Child Controls - Why are their IDs null?

I want to create a custom control ideally like this:
<foo:Frobnicate runat="server">
<DoStuffWithThese>
<asp:TextBox runat="server" ID="fizzbot" />
</DoStuffWithThese>
<!-- other controls can be in the wrapper but outside the DoSomeStuffWithThese collection -->
<asp:DropDownList runat="server" ID="othercontrol" />
</foo:Frobnicate>
although I would settle for this if necessary:
<foo:Frobnicate runat="server">
<DoStuffWithThese>
<asp:TextBox runat="server" ID="fizzbot" />
</DoStuffWithThese>
<AndOtherStuffWithThese>
<asp:DropDownList runat="server" ID="othercontrol" />
</AndOtherStuffWithThese>
</foo:Frobnicate>
I can access the controls in the codebehind OK (in the second, not-ideal example), but for some reason their IDs (which I need) are NULL.
Here is the codebehind:
[ParseChildren(true)]
public class Frobnicate : WebControl {
[PersistenceMode(PersistenceMode.InnerProperty)]
public List<WebControl> DoStuffWithThese { get; set; }
[PersistenceMode(PersistenceMode.InnerProperty)]
public List<WebControl> AndOtherStuffWithThese { get; set; }
public override OnLoad(EventArgs e) {
base.OnLoad(e);
foreach(Control currentControl in DoStuffWithThese) {
// the control can be accessed (e.g. I can see it's a TextBox, etc.
// but currentControl.ID == null here -- why? :(
}
}
Does anyone know why this is? And more importantly, how I can fix it so I can get a custom control in one of those formats and able to access the IDs?
I believe you have to implement INamingContainer in your class, WebControl doesn't do that on its own. Note that it's just a marker interface, you don't need any code. Also, instead of a list of controls, you should probably use ITemplate (which allows you to actually create the controls and use them in the output of your control).
This should be helpful: http://msdn.microsoft.com/en-us/library/36574bf6(v=vs.85).aspx
If this isn't what you want, please elaborate on what you're actually trying to do, and why you don't want to use templates.
To explain a bit more - just by adding a reference to a server control, you're not actually adding it anywhere to the page / control tree, which means it isn't actually created, and it doesn't work as part of the page, really (which includes a useful UniqueID/ClientID, and pretty much any logic except for the constructor).
When you have the template, you can use data binding to fill in the required data etc., and you can access the controls using FindControl, for example (or just use the Controls collection, but be aware that the template will likely also contain literals and other stuff than just your controls).
Can you get the id of the dropdown? If so, I think it has to do with the other items missing runat=server.

ASP.net How to output cache a webusercontrol on controls public properties

I have a web user control, it serves some potentially intensive data calculations and I would like it to be output cached so that each page view doesn't recalculate the data. It resides on very frequently viewed pages so it's quite important I get it working right!
For context, it's used on our arcade:
http://www.scirra.com/arcade/action/93/8-bits-runner
Click on stats, the data for the graphs and stats are generated from this webusercontrol.
The start of the control is as follows:
public partial class Controls_Arcade_Data_ArcadeChartData : System.Web.UI.UserControl
{
public int GameID { get; set; }
public Arcade.ChartDataType.ChartType Type { get; set; }
protected void Page_Load(object sender, EventArgs e)
{
Now the difficulty I'm having is the output cache needs to be dependant on both the GamID and the ChartType.
This control is re-used with many different combinations of GameID's and Types, I need it to create a cache for each of these but am struggling to find out how to do this.
For example, one arcade game might pass in GameID = 93 and Type = GraphData, another might be GameID = 41 and Type = TotalPlaysData and another might be GameID = 93 but Type = TotalPlaysData. These should all return different data and have different output caches.
The control is used on the games page sort of like this (the parameters are actually set in the codebehind)
<div>Total Plays:</div>
<div class="count"><Scirra:ArcadeChartData runat="server" GameID="93" Type="TotalPlays" /></div>
<br /><br />
<div>Total Guest Plays:</div>
<div class="count"><Scirra:ArcadeChartData runat="server" GameID="93" Type="TotalGuestPlays" /></div>
etc.
Any help appreciated! I've spent a while looking online and it's kept coming up as something I need to solve but can't figure this one out.
Edit
Edit: I've tried adding this line to my control:
<%# OutputCache Duration="20" VaryByControl="GameID;Type" %>
But it just throws the error Object reference not set to an instance of an object. on the line where GameID is being set for the first time on the ASPX page using the control.
When a Control is retrieved from the output cache, it's not instantiated as an instance that you can manipulate; you just get the output the Control generated, not the Control itself. For example, you can't set properties on a cached Control from code behind, as you said in your question. The vary-by properties should be set declaratively (using an ExpressionBuilder might also work, though I haven't tried it).
To see in code behind whether a control has been retrieved from the output cache, check for null:
if (this.YourControlID != null) // true if not from cache
{
// do stuff
}
Even with that caveat, Control output caching is a bit quirky.
Try this:
<%# OutputCache Duration="20" VaryByControl="GameID;Type" Shared="true" %>
The output of the Control is stored in the output cache by associating it with a certain key. With Shared="true", the cache key is the value of all specified properties, together with the Control's ID. Without Shared="true", the cache key also includes the Page type, so the output would vary by Page -- which doesn't sound like what you want.
If you use the Control on more than one page, be sure to use the same ID on each page if you can, since the ID is included as part of the key for the output cache. If you can't or don't use different IDs, you will get a new copy of the Control's output in the cache for each unique ID. If the Controls with different IDs always have different property values anyway, that may not be an issue.
As an alternative to the OutputCache directive, you can set an attribute on the class declaration:
[PartialCaching(20, null, "GameID;Type", null, true)]
public partial class Controls_Arcade_Data_ArcadeChartData : UserControl
You need to take the following steps:
1) Add the following output cache directive to the page:
<%# OutputCache Duration="21600" VaryByParam="None" VaryByCustom="FullURL" %>
2) Add the following to global.asax:
public override string GetVaryByCustomString(HttpContext context, string arg)
{
if (arg.Equals("FullURL", StringComparison.InvariantCultureIgnoreCase)
{
// Retrieves the page
Page oPage = context.Handler as Page;
int gameId;
// If the GameID is not in the page, you can use the Controls
// collection of the page to find the specific usercontrol and
// extract the GameID from that.
// Otherwise, get the GameID from the page
// You could also cast above
gameId = (MyGamePage)oPage.GameID;
// Generate a unique cache string based on the GameID
return "GameID" + gameId.ToString();
}
else
{
return string.Empty;
}
}
You can get more information on the GetVaryByCustomString method from MSDN and also review some of the other caching options here.
create a cache object in the code
HttpRuntime.Cache.Insert("ArcadeChartData" + GameID + Type, <object to cache>, null, System.Web.Caching.Cache.NoAbsoluteExpiration,new TimeSpan(0, 0, secondsToCache),CacheItemPriority.Normal, null);
above cache item will be enough to your work, but if you really want to use output cache as well try following code in the code behind,
Response.AddCacheItemDependency("ArcadeChartData" + GameID + Type);
Response.Cache.SetExpires(DateTime.Now.AddSeconds(60));
Response.Cache.SetCacheability(HttpCacheability.Public);
Response.Cache.SetValidUntilExpires(true);
Setting values for the page output cache is the same as manipulating
the SetExpires and SetCacheability methods through the Cache property.
I know that my solution may look very simple and possibly weird but I tried it and it works.
You simply have to add this line in your UserControl.
<%# OutputCache Duration="10" VaryByParam="none" %>
Note : I have only tested the Framework 4.0. Also if ever you have to change the value of the property in the UserControl (MyInt, My String in this example) do it in the Page_Init event.
Here is all my code :
Page :
<%# Page Title="Home Page" Language="vb" MasterPageFile="~/Site.Master" AutoEventWireup="false" CodeBehind="Default.aspx.vb" Inherits="MyWebApp._Default" %>
<%# Register Src="~/UserControl/MyUserControl.ascx" TagPrefix="uc" TagName="MyUserControl" %>
<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
<uc:MyUserControl ID="uc1" MyInt="1" MyString="Test" runat="server" />
<hr />
<uc:MyUserControl ID="uc2" MyInt="3" MyString="Test" runat="server" />
<hr />
<uc:MyUserControl ID="uc3" MyInt="1" MyString="Testing" runat="server" />
</asp:Content>
User Control:
<%# Control Language="vb" AutoEventWireup="false" CodeBehind="MyUserControl.ascx.vb" Inherits="MyWebApp.MyUserControl" %>
<%# OutputCache Duration="10" VaryByParam="none" %>
<div style="background-color:Red;">
Test<br />
<asp:Label ID="lblTime" ForeColor="White" runat="server" />
</div>
User Control Code:
Public Class MyUserControl
Inherits System.Web.UI.UserControl
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Debug.Write("Page_Load of {0}", Me.ID)
Dim oStrBldr As New StringBuilder()
For i As Integer = 1 To Me.MyInt
oStrBldr.AppendFormat("{0}: {1} - {2} at {3}<br />{4}", Me.ID, i, Me.MyString, Date.Now.ToLongTimeString(), System.Environment.NewLine)
Next
Me.lblTime.Text = oStrBldr.ToString()
End Sub
Public Property MyInt As Integer
Public Property MyString As String
End Class
Please keep me posted, I have other solutions if ever you wish but they are more complex. I may also post with C#
One easy trick is to put all the graphics in a new page receiving GameId and Type as querystring parameters, use the out-of-the-box output cache for querystring parameters and the put an iframe in your page. Also you can make use of the browser's cache and never get the server hit for a while.
Ok, well the reason why this so hard to make OutputCache work in this case is because it wasn’t design to be use with Property’s, however it works very well with QueryString parameters. So my next solution isn’t the most professional and probably not the best, but it is definitely the fastest and the one that requires less code changing.
Since it works best QueryString, I recommend you putting your UserControl in one blank page, and wend ever you want to use your UserControl make an iframe that links to your page with the UserControl with QueryString.
Where you want to use your UserControl:
<iframe src="/MyArcadeChartData.aspx?GameID=93&Type=TotalPlays"></iframe>
Full page markup, MyArcadeChartData.aspx
<%# Page ... %>
<%# OutputCache Duration="20" VaryByParam="GameID;Type" %>
<Scirra:ArcadeChartData ID="MyUserControlID" runat="server />
Full page code, MyArcadeChartData.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
//TODO: Put validation here
MyUserControlID.GameID = (int)Request.QueryString["GameID"];
MyUserControlID.Type = (YourEnum)Request.QueryString["Type"];
}
Please not that values in the QueryString can be seen by the user, please do not put sensitive data.
Also I’m aware that this isn’t the most professional solution, but it is the easiest to implement, from what I know.
Regards and happy holidays
If I understand right, the caching isn't working correctly because of the way you have the properties supplying the values to the control, which probably has to do, in part, with the calculations that are being done.
You could create an HttpHandlerFactory that takes the request, does your calculations if they're not in the cache (inserting into the cache afterwards), handles the expiration of values, and then passes the request on to the page. It wouldn't be control-specific at all. That way you could use these calculated values in any control or page, and wouldn't have to implement caching policies that worry about their own calculations.
If this isn't data intensive, have you considered storing it in the Session as apposed to caching it? Just a thought...
Arcade.ChartDataType.ChartType Type;
string GameKey = GameId + Type.toString();
storedData = callCalculation(GameId,Type);
Session[GameKey] = storedData;
I realize this isn't in the cache, I am just trying to be constructive.

Dynamically adding controls to a Templated User Control?

A few weeks back I created a Templated User Control, for the most part based on the example here:
http://msdn.microsoft.com/en-us/library/36574bf6(v=VS.90).aspx
The difference is that I did not implement the "MessageContainer" class as I wanted just an empty template that I could add whatever controls I wanted to at design time.
This TUC has been working great, but I ran into a scenario I hadn't anticipated when I created the thing. The need to dynamically add this TUC to a page, which means that I would need to dynamically add the controls within the template of the TUC as well.
I found another example here on how to dynamically create a Template and add it to the Templated Control:
http://msdn.microsoft.com/en-us/library/y0h809ak(v=vs.71).aspx
This second example article discusses only the "DataList, Repeater, and DataGrid controls" but I figure since I am using the ITemplate interface here, it should be the same thing.
However, I am unable to get this to work, I keep getting an "Object reference not set to an instance of an object." error when I attempt to populate the TUC.
Here's what I am doing....
Like the example above I created an ITemplate class:
public class XPCTemplate : ITemplate
{
private readonly Control _control;
public XPCTemplate(Control control)
{
_control = control;
}
public void InstantiateIn(Control container)
{
container.Controls.Add(_control);
}
}
Then, in the test page code-behind I attempt to load up and display the TUC dynamically:
ExpandCollapseRegion xpcRegion; // The Templated User Control
protected void Page_Load(object sender, EventArgs e)
{
PlaceHolder ph;
// .... code here to dynamically create some labels, textboxes, etc. ....
// Create an instance of the TUC
xpcRegion = new ExpandCollapseRegion();
// Pass into the TUC's template the controls dynamically created above
xpcRegion.LayoutTemplate = new XPCTemplate(ph);
// add the dynamic TUC to the page
phDynamicUC.Controls.Add(xpcRegion);
phDynamicUC.Controls.Add(new LiteralControl("<br />"));
}
Test page HTML Source:
<body>
<form id="form1" runat="server">
<div>
Dynamically Loading User Control
<br />
<asp:PlaceHolder ID="phDynamicUC" runat="server" />
</div>
</form>
</body>
When I run the page, I get the "Object reference not set to an instance of an object" error on the "container.Controls.Add(_control);" line of the XPCTemplate class. When I debug the test page and TUC control, the code of the TUC receives the XPCTemplate into its LayoutTemplate during the TUC's Page_Init() method, but when the InstantiateIn() event back in the XPCTemplate fires immediately afterwards, the "container" argument is NULL.
I'm not sure why this is happening, it's like the InstantiateIn() method of the XPCTemplate class is trying to actually set the PlaceHolder control within the TUC rather than just passing the contents. Maybe this is supposed to be the way and I am missing something on the TUC side to allow this behavior?
This is the first TUC I have created and likewise the first time trying to dynamically fill/load it, so I am sure I am missing something needed to accomplish this. Any help is greatly appreciated.
-- Andrew
Found the solution to the problem, which was how I was loading the TUC.
Incorrect:
xpcRegion = new ExpandCollapseRegion();
Correct:
xpcRegion = (ExpandCollapseRegion)LoadControl("ExpandCollapseRegion.ascx");
Making this simple change took care of the problem. Also, found that I could forego the need for a custom ITemplate class by using the CompiledTemplateBuilder() method. Really simplifies the code.
-- Andrew
I believe the problem is that you haven't instantiated your PlaceHolder ph that you are passing into your template. So, the line it is complaining on is because it can't add the control to the PlaceHolder because it is null/nothing.
Also, I believe you're going to need to put your placeholder somewhere. Otherwise, you're adding items to a container that is never added to the page.

How to add same behaviour to a usercontrol template as the gridview <Columns>

When defining the markup for an asp gridview and the tag Columns, one can only choose from a predefined set of controls to add within it (asp:BoundField, asp:ButtonField etc).
Im curious about if i can add the same type of behavior, say restricting the content to a custom control with the properties "Text" and "ImageUrl" to a TemplateContainer defined in a standard usercontrol and then handle the rendering of each element within the container from code behind somehow?
Alright i finally solved it, which means i can do the following
<%# Register src="~/Controls/Core/ContextMenu.ascx" tagname="ContextMenu" tagprefix="uc" %>
<%# Register Assembly="App_Code" Namespace="Core.Controls.ContextMenu" TagPrefix="cc" %>
<uc:ContextMenu ID="ContextMenuMain" runat="server">
<Items>
<cc:ContextMenuItem Text="New" ImageUrl="..." />
<cc:ContextMenuItem Text="Save" ImageUrl="..." />
</Items>
</uc:ContextMenu>
Where each ContextMenuItem is a custom class in app code, notice that i have to register the app_code assembly in order for the markup to recognize the class.
The namespace points to the location of the class.
For the code behind of the usercontrol we just add this:
private List<ContextMenuItem> items = new List<ContextMenuItem>();
[PersistenceMode(PersistenceMode.InnerProperty), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public List<ContextMenuItem> Items
{
get
{
if (items == null)
{
items = new List<ContextMenuItem>();
}
return items;
}
set
{
items = value;
}
}
Which can be processed by the usercontrol when its time to render :)
FYI The fields (asp:BoundField, asp:ButtonField etc) are not actually controls but are instead derived from the DatControlField class. Likewise, the columns property is not a ITemplate but is a DataFieldCollection.
Something like that should be possible if your controls all derive from the same class or implement the same interface.
I'm thinking maybe you want something like this:
http://www.developer.com/net/asp/article.php/10917_3609991_1
If you want to create a custom column type for a GridView, you need to start out by deciding what goes in each cell of the column. Is it just a TextBox or Button? Or is it something more complicated? If it's more complicated, you need to create your own template class by inheriting from ITemplate.
Once you have that done, you need to write a class that inherits from the proper column type (BoundField, ButtonField, etc. for simple columns or TemplateField if you need to plug in your own template).
You can create your own properties for your custom column class (e.g., Text and ImageUrl), but if it's a TemplateField, you will have to actually loop through your cells to manipulate the data of the controls that comprise each cell. This is not hard to do, but it's not as easy as just using a BoundField and setting DataField property.

ASP.NET: generate property value when adding control to page

Context: ASP.NET 3.5 / C#
Hi,
I created a user control
public partial class MyControl : UserControl
{
// EDIT: example first used "UniqueId" as property name, which was wrong.
public Guid MyId { get; set; }
// ...
}
and this example usage
<uc1:MyControl
ID="myControl"
MyId="443CBF34-F75F-11DD-BE2F-68C555D89123"
runat="server" />
Steps:
Add this control to a web form (aspx)
Expected result:
the HTML for the user control is added, a unique value (corresponding to Guid.NewGuid()) for MyId is set in the ASPX HTML at design-time as the MyId attribute value.
Actual result:
the HTML for the user control is added, a unique value for MyId is not set in the HTML at design time for the MyId Attribute value.
If this is not possible:
Workaround 1: Would it be possible to achieve this using a server control? How?
Workaround 2: is it possible to achieve this using a UserControl design-mode task?
Clarification:
persisting the property value is not an issue, since it never changes for a control intance and is automatically set by ASP.NET through the control declaration in the aspx page.
the MyId attribute does not need to be rendered at runtime.
Gr B!
Custom Design-time Control Features in Visual Studio .NET
You have a couple problems here, but first I will answer your questions about the workarounds.
No you are already using a server control.
No design-mode is to just make the lives of the developer easy, it doesn't effect anything else
You have two problems here. There is already a property called UniqueID I don't know if you were trying to overload that, but the question wasn't clear. The second problem is that your UniqueID essentially not getting stored anywhere. Try the following code:
public Guid UniqueId {
get { return (Guid)ViewState["MyUserControlUniqueId"]; }
set { ViewState["MyUserControlUniqueId"] = value; }
}
That will store the GUID in the ViewState so that you can retrieve it on post backs.
Update: Given your comment you need to override/use this method to add attributes to the rendered content.
http://msdn.microsoft.com/en-us/library/system.web.ui.webcontrols.webcontrol.addattributestorender.aspx
If I understand your question correctly, you are exposing a property on your user control called MyId. This allows you to set the property wherever you put that control.
What you also want, is for the rendered HTML to also include this attribute and value.
If that's the case, the property MyId is not passed through to the HTML, it's only because the user control has MyId as a property that it's visible in the designer.
In your user control you will have defined the markup that gets rendered.
So for example if you have:
<asp:Panel runat="Server" Id="myControlDiv">Some other content</asp:Panel>
You can then in your controls prerender event (or wherever else you choose) put
myControlDiv.Attributes.Add("MyId", SomeGuid.ToString())
Which will then get output in the HTML as
<div id="generatedID" MyID="443CBF34-F75F-11DD-BE2F-68C555D89123">Some other content</div>
So you just want a unique ID generated that you only ever use in design time?
Why not override Object.GetHasCode();
And then exposure this as a property?

Categories

Resources