Repeater template control with custom events - asp.net

I'm writing item template for repeater in separate control, and then I'm using following code:
HospitalRepeater.DataSource = LocationsList;
HospitalRepeater.ItemTemplate = Page.LoadTemplate("~/UserControls/HospitalDetails.ascx");
HospitalRepeater.DataBind();
This code worked fine, however now we want to add custom events to HospitalDetails control.
We created following event with custom event args:
public class HospitalItemEventArgs : EventArgs
{
public Int32 HospitalID { get; set; }
public HospitalItemEventArgs() { }
public HospitalItemEventArgs(Int32 hID)
{
this.HospitalID = hID;
}
}
public event EventHandler<HospitalItemEventArgs> HospitalAction;
protected virtual void OnHospitalAction(HospitalItemEventArgs e)
{
if (HospitalAction!= null)
this.HospitalAction(this, e);
}
Now here is a problem - I can't access this custom event from my code after loading this control as template, because it returns object of System.Web.UI.ITemplate.
I assumed that this is wrapper above my exact control, but this assumtion is wrong.
Cast to my control type fails with following error message:
Unable to cast object of type 'SimpleTemplate' to type 'UserControls.HospitalDetails'.
I've tried reverse action - load control using
Page.LoadControl("~/UserControls/HospitalDetails.ascx");
It returns object of correct HospitalDetails type, but It does not implement ITemplate interface.
Whan I tried to do that I've received error message:
Unable to cast object of type 'ASP.usercontrols_hospitaldetails_ascx' to type 'System.Web.UI.ITemplate'.
Can anyone help me to deal with this cast, or find another solution which matches following requrements:
Repeater should be bound to list of HospitalItems to display details.
Template for displaying details should hide it's UI interactions and expose few simple events like HospitalAction with hospital ID.
Separate object must have ability to subscribe to this events.

Your best bet is probably to create some <asp:Button /> controls specifying the CommandName and CommandArgument attributes. You can then handle the bubbled even on the repeater itself, interrogate the arguments for CommandName and CommandArgument.
I'm not using a separate control as a template but the same thing can be accomplished like this.
<asp:Repeater ID="myRepeater" runat="server">
<ItemTemplate>
<asp:Button ID="SaveButton"
CommandName="Save"
CommandArgument="{insert hospitol ID here}"
Text="Save"
runat="server" />
</ItemTemplate>
</asp:Repeater>
void myRepeater_ItemCommand(object source, RepeaterCommandEventArgs e)
{
if (e.CommandName == "Save")
{
int id = int.Parse(e.CommandArgument.ToString());
//Do some saving
}
}
Update
The purpose for the button's CommandArgument and CommandName attributes is to allow you to provide the user with individual actions to take on databound ui content. The CommandArgument attribute is to allow you a point of entry back into the data to retrieve the relevant information for the event. I have never tried to put multiple values in this attribute, but I do not see a reason why it would not work.
Another option is to create a separate list that contains HospitalID and DoctorID associations, create a unique ID for each association and store that list in a database (if you need to persist it), cache (if used by multiple users), session (if used by multiple pages by same user) or Viewstate (if used by a single page and the list is relatively short).

Related

How do I declaratively bind 'SelectedValue' to datasource field?

I want to drive a RadioButtonLists SelectedValue property from a property in my datasource, but I'm not managing to make it work.
I have a <asp:RadioButtonList on my page bound to an <asp:ObjectDataSource. This datasource in turn provides the following model class:
public class CollectionWithDefault : Collection<string>
{
CollectionWithDefault(IEnumerable<string> items, string defaultItem)
{
foreach (var item in items)
Add(item);
DefaultItem = defaultItem;
}
public string DefaultItem { get; }
}
Notice that this class is a standard collection of strings that also exposes which one of them is the default option.
Consider that I have the following implementation for a value provider. This is a simple in-memory implementation, but keep in mind that this could be coming from a database or any other source:
public static class ItemProvider
{
public static CollectionWithDefault GetAvailableItems()
{
var items = new [] { "option1", "option2", "option3" };
return new CollectionWithDefault(items, items[1]);
}
}
I tried the following:
<asp:ObjectDataSource runat="server"
ID="ItemSource"
TypeName="MyNamespace.ItemProvider"
SelectMethod="GetAvailableItems" />
<asp:RadioButtonList runat="server"
DataSourceID="ItemSource"
SelectedValue='<%# Eval("DefaultItem") #>' />
I'm getting the following exception in the Eval call:
System.InvalidOperationException: 'Databinding methods such as Eval(), XPath(), and Bind() can only be used in the context of a databound control.'
How can I ensure that the correct radio is preselected based on the field coming from my datasource?
Changing the collection model itself to make it work is acceptable, but I can't set the SelectedValue from codebehind. I wanted to rely on the datasource control to do the heavy lifting.
I managed to make this work seamlessly without requiring manual assignments in codebehind by extending the original RadioButtonList control and modifying the core databinding method to honor ListItem objects.
It goes something like this:
public class MyRadioButtonList : RadioButtonList
{
protected override void PerformDataBinding(IEnumerable dataSource)
{
if (dataSource is IEnumerable<ListItem> listItems)
{
...
foreach (var listItem in listItems)
Items.Add(listItem);
...
}
else
{
base.PerformDataBinding(dataSource);
}
}
}
With this in place, it was just a matter of converting my source model into a IEnumerable<ListItem> on the presentation layer (easy to accomplish with an adapter/proxy implementation) and then feed these ListItems to the control.
Once I got this in place, I could see my selected items reflected correctly in the UI based on the datasource field. Considering how trivial the extension is, I feel it was quite worth it :)
The same inheritance approach can probably be used for similar controls like CheckBoxList, which suffers from the very same limitation.
For the more adventurous folks, one could also make this work by introducing extra DataSelectedField and DataEnabledField properties in the control and using Eval on top of them as part of the original databinding algorithm (which already does this with DataTextField and DataValueField). I felt this would be a little bit more involved for my use case and decided to go with a simpler override, but it is definitely a valid approach that could even live along my proposed solution for an even more robust RadioButtonList.

ASP.NET ASCX Use of Instance Variable

Let's say I have an ASCX user control that requires access to the current user's full name. An ASPX page contains this line at the top
<%# Register src="top.ascx" tagprefix="custom" tagname="top" %>
and this line in the body:
<custom:top runat="server" />
The ASPX file knows the user ID of the current user and could determine his full name. So how can I use the code run by the ASPX file to provide its information to the ASCX file?
Declare a property on the UserControl and have the parent page set it.
On your usercontrol:
public string FullName { get; set; }
On the aspx page either set it in the code behind
YourUserControl.FullName = FullName
or through markup
<custom:top runat="server" FullName="<%= SomeProperty %>" />
You could use the Page property of the user control and cast it to the instance of your page. Then, call the method of your page class to get the user name.
To make this work in a dynamically compiled project, you have to do a little more work to have the control recognize the data type of the dynamically compiled page. Here is a short tutorial on how to do.
Or, as Brandon outlines, do the opposite and let your page tell your user control the information.
This sounds like you might be mistaken about how the page lifecycle works and how you can expose data across your controls. For example, lets say you have this code in your ASPX:
public override void OnLoad(EventArgs e)
{
string userName = "Bob";
}
In your ASPX file, you can reference the control and set a property on it to pass the data along:
<custom:top ID="someControl" runat="server" />
You expose a property in your top control like so:
public string UserName { get; set; }
You could then add this code to your OnLoad method:
someControl.UserName = userName;
Then your control will have access to that data. Alternatively, you can stick things in the Request cache if you dont have a direct line to the control:
HttpContext.Current.Items["key"] = userName;
And then pull the data from your control via the same fashion:
string fromCache = HttpContext.Current.Items["key"];
You could go about this in several ways. I typically use a session variable, since the user will be bound to the session.
In the ASPX (or when the user logs in):
Session["UserFullName"] = GetFullName(); //Code to get full name here
In the ASMX:
this.FullName = Session["UserFullName"]; //TODO: Check for null values

How do I get many property values from View to Presenter in WebFormsMvp?

What is the best way to get a number of property values of a business object from the View to the Presenter in a WebFormsMvp page?
Bearing in mind this issue with DataSources.
Here is what i propose:
The scenario is, I have a business object called Quote which i would like to load form the database, edit and then save. The Quote class has heaps of properties on it. The form is concerned with about 20 of these properties. I have existing methods to load/save a Quote object to/from the database. I now need to wire this all together.
So, in the View_Load handler on my presenter i intend to do something like this:
public void View_Load(object sender, EventArgs e)
{
View.Model.Quote = quoteService.Read(quoteId);
}
And then bind all my controls as follows:
<asp:TextBox ID="TotalPriceTextBox" runat="server"
Text="<%# Model.Quote.TotalPrice %>" />
All good, the data is on the screen.
The user then makes a bunch of changes and hits a "Submit" button. Here is where I'm unsure.
I create a class called QuoteEventArgs exposing the 20 properties the form is able to edit. When the View raises the Submit button's event, I set these properties to the values of the controls in the code behind. Then raise the event for the presenter to respond to. The presenter re-loads the Quote object from the database, sets all the properties and saves it to the database.
Is this the right way to do this? If not, what is?
"A nicer way" (/alternative) is to make use of the 2-way binding, therefore what will be passed back to the Presenter for processing will be your Quote object.
This can be achieved through the use of an asp:FormView in conjunction with the mvp:PageDataSource that specifies an UpdateMethod and the Bind() method.
The WebFormsMVP sample project demonstrates this via the 'EditWidgetControl', including the methods required on the View code-behind file.
As an option your view can simply implement only the EditItemTemplate for asp:FormView making use of DefaultMode="Edit" on the FormView.
Sample Structure:
<asp:FormView DataSourceID="theSource" DefaultMode="Edit">
<EditItemTemplate>
<fieldset>
<asp:TextBox id="totp" value='<%# Bind("TotalPrice") %>' runat="server" />
</fieldset>
</EditItemTemplate>
</asp:FormView>
<mvp:PageDataSource ID="theSource" runat="server"
DataObjectTypeName="Your.NameSpace.Quote"
UpdateMethod="UpdateQuote">
</mvp:PageDataSource>
Code-behind:
public void UpdateQuote(Quote q, Quote ori)
{
OnUpdatingQuote(q, ori);
}
public event EventHandler<UpdateQuoteEventArgs> UpdatingQuote;
private void OnUpdatingQuote(Quote q, Quote ori)
{
if (UpdatingUserGroup != null)
{
UpdatingUserGroup(this, new UpdateQuoteEventArgs(q, ori));
}
}
How to use the GridView inside a FormView.
Because I have list to populate the grid in a entity.

FormView + FileUpload - can I change a bound field based on the fileupload?

I have a FormView control in an ASP.NET 2.0 app. I've got the database storing a filename (a person's picture) in a column. I can't bind the value of the column to a fileupload control - so I'm trying to use a hidden form field. Here's what I have:
<asp:HiddenField ID="pictureLink" runat="server" Value='<%# Bind("pictureLink") %>' />
<asp:FileUpload ID="pic" runat="server" />
Code Behind:
//ItemUpdating event handler
void do_update(object sender, FormViewUpdateEventArgs e)
{
FileUpload newpic = (FileUpload)profile_edit.FindControl("pic");
if (newpic.HasFile)
{
//do a bunch of file uploading "stuff" which makes a new file name
e.Keys["pictureLink"] = new_filename;
}
}
My goal is to update the hidden form field's value to the newly updated file name so the database is properly updated.
I think I'm close - but it seems like I can't programmatically alter any of the bound data fields after-the-fact.
I've tried using javascript to change the control - but the new file name will actually be different than what they upload; which javascript can't necessarily "predict" and reliably put the correct file name into the hidden form field
Any suggestions?
Thanks
I think you might need to alter e.NewValues, not e.Keys. See the NewValues property on MSDN, it might point you in the right direction.
OK - I found the answer not too long after I posted the question. I'll leave it open in case someone has a better (more elegant) solution. Basically, I change the do_update event handler to intercept the file upload. If there's a file, then I edit the NewValues collection so that the database receives the new file name instead of the old one.
//ItemUpdating event handler
void do_update(object sender, FormViewUpdateEventArgs e)
{
FileUpload newpic = (FileUpload)profile_edit.FindControl("pic");
if (newpic.HasFile)
{
//do a bunch of file uploading "stuff" which makes a new file name
//HERE IS THE CHANGE - update the newvalues object to the new file name
e.NewValues[1] = new_filename;
}
}

How can I stack an unknown number of ASP.NET Repeaters?

I am building a pop-out menu, and the client wants it to be able to pop out continously based on a heirarchy.
For example, the first pane is a list of options. When they are hovered over, another pane should pop up next to it with the next level of options, and so on until the last level of options is reached.
I can handle all the javascript and stuff, but I can't think of a way to continously embed repeaters inside repeaters. I know I could do it once by putting a repeater inside another, but then I would only have two layers.
I need to be able to continously embed repeaters for each layer of options, or achieve this with a similar technique using a different control.
Any help is great, thanks!
You won't be able to build this in mark up. You'll have to add the controls dynamically in your code behind by building the Repeater for each level and adding it to the previous Repeater's template. It will require a full postback for each option selected because the nested Repeater could potentially be of different depth depending on which option is chosen.
You might be better off doing this all client-side, though, using AJAX and javascript. When an option is chosen, fire off an AJAX request to see if that option has sub-options. If it does (return them), then dynamically build the new options control using javascript and add it to the page. When a different option is chosen, you'll remove the elements from the DOM holding the previously chosen options sub-options.
If you can get your menu out in form of a list of MenuItem objects, each of which has a (sometimes empty) list of sub items (and I really mean a List<MenuItem> here... we're going to use this collection as a datasource for a sub-repeater, so it needs to implement IEnumerable<T>) as a property MenuItem.SubItems, you could probably make use of a UserControl that loops out one menu level, and calls upon itself for the next.
In the UserControl you'd have something like this:
<li><a href='<%= this.MenuItem.Url %>'><%= this.MenuItem.LinkText %></a></li>
<asp:Repeater ID="UCRepeater" runat="server">
<HeaderTemplate>
<ul>
<ItemTemplate>
<menu:MenuItem ID="MenuItemUC" runat="server" />
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
The UserControl in the ItemTemplate is the same one, so for each item template the same thing will be rendered.
Below is the Code Behind for this user control, and this is where the magic happens:
public partial class MenuItemUserControl : UserControl
{
// A property we'll use as the data source
public MenuItem MenuItem { get; set; }
protected void Page_Load(object sender, EventArgs e)
{
// If the current menu item has sub items, we bind the repeater to them
// And by the way, there is no use doing this on every postback. First
// page load is good enough...
if(!Page.IsPostBack) {
{
if(MenuItem.SubItems.Count > 0)
{
UCRepeater.DataSource = MenuItem.SubItems;
UCRepeater.DataBind();
}
}
}
protected void UCRepeater_OnItemDataBound(object sender,
RepeaterDataBoundEventArgs e)
{
// Every time an Item is bound to the repeater, we take the current
// item which will be contained in e.DataItem, and set it as the
// MenuItem on the UserControl
// We only want to do this for the <ItemTemplate> and
// <AlternatingItemTemplate>
if(e.Item.ItemType == ListItemType.Item ||
e.Item.ItemType == ListItemType.AlternatingItem)
{
var uc = (MenuItemUserControl)e.Item.FindControl("MenuItemUC");
if(uc != null)
{
// This is the magic. Abrakadabra!
uc.MenuItem = (MenuItem)e.DataItem;
}
}
}
}
So in order to get this to work, the only thing missing is really a nice way of getting your data out as a hierarchical list of MenuItems. This I'll leave to your data access layer (and it would be cheap easy using LINQ to SQL or Entity Framework... ;) )
DISCLAIMER: This code is provided as is, and I wrote it off the top of my head. I have not tested it, but I think it will work - and if it doesn't, it could at least give you an idea of how to solve the problem. If you have problems, please post them in comments and I'll try to help out - but there are no promises of success here. Just a willingness to help out! =)

Resources