How to add a bindable property in my user control? - asp.net

So I created an ASP.NET user control that is a simple wrapper around the bootstrap slider control. Now I want my control to have a public property named Value that I could bind to a property of my model, just like I do with asp:TextBox:
<asp:TextBox runat="server" Text="<%# BindItem.Name %>" />
Can I do the same with my user control?
<uc1:Slider runat="server" Value="<%# BindItem.Age %>" />

So for any future reader, here's a working version of bootstrap-slider's server-side control. You can drag-n-drop it onto your ASPX page. It has two properties Value and Value2 (for ranges). There is an event ValueChanged that will fire when user moves the position of slider. You can attach an event handler to this even just like you do for Button control's Click event. Last but not the least, I ensured that the control works within UpdatePanel too:
N.B. This control doesn't try to include the require JS or CSS file, because if user drops multiple instances of the Slider on the page, the output will probably include a copy of CSS and JS file for each instance, which is undesirable. I couldn't figure out how to link a CSS/JS file only if it hasn't already been included in the current page. So just make sure you include those two files in the parent page once for all.
Markup
<div id="<%= this.UniqueID + "_Div" %>">
<input runat="server" ID="_TextBox"
EnableTheming="false"
class="slider form-control"
style="vertical-align: middle; width: 100%;"
data-slider-min="<%# Min %>"
data-slider-max="<%# Max %>"
data-slider-step="<%# Step %>"
data-slider-value="<%# Value %>"
data-slider-precision="<%# Precision %>"
data-slider-orientation="horizontal"
data-slider-selection="after"
data-slider-tooltip="show"
data-slider-range="<%# IsRange.ToString().ToLower() %>" />
</div>
Code-behind
using System;
using System.ComponentModel;
using System.Web.UI;
namespace YourProjectNameSpace
{
public partial class Slider : System.Web.UI.UserControl
{
public bool IsRange { get; set; }
public float Min { get; set; }
public float Max { get; set; }
public float Step { get; set; }
public int Precision { get; set; }
public event Action ValueChanged;
[Bindable(true, BindingDirection.TwoWay)]
public float Value
{
get
{
if (IsRange)
{
string[] Vals = (this._TextBox.Attributes["value"] ?? "0,0").Split(',');
return float.Parse(Vals[0]);
}
else
return float.Parse((this._TextBox.Attributes["value"] ?? "0"));
}
set
{
if (IsRange)
{
string[] CurVals = (this._TextBox.Attributes["value"] ?? "0,0").Split(',');
this._TextBox.Attributes["value"] = value.ToString() + ',' + CurVals[1];
}
else
this._TextBox.Attributes["value"] = value.ToString();
}
}
[Bindable(true, BindingDirection.TwoWay)]
public float? Value2
{
get
{
if (IsRange)
{
string[] Vals = (this._TextBox.Attributes["value"] ?? "0,0").Split(',');
return float.Parse(Vals[1]);
}
else
return null;
}
set
{
if (IsRange)
{
string[] CurVals = (this._TextBox.Attributes["value"] ?? "0,0").Split(',');
this._TextBox.Attributes["value"] = CurVals[0] + ',' + value.ToString();
}
}
}
protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack)
{
if (Request["__EVENTTARGET"] == "_TextBox")
{
if (ValueChanged != null)
ValueChanged();
string MyDivName = this.UniqueID.Replace("$", #"\\$") + "_Div";
string SliderVal = Request["__EVENTARGUMENT"];
if (IsRange) SliderVal = '[' + SliderVal.Split(',')[0] + ',' + SliderVal.Split(',')[1] + ']';
Page.ClientScript.RegisterStartupScript(this.GetType(), "CreateSliderFor_" + MyDivName,
"$('#" + MyDivName + " > input').slider().slider('setValue', " + SliderVal +
").on('slideStop', function(slideEvt) { __doPostBack('_TextBox', slideEvt.value); });", true);
}
else
{
string MyDivName = this.UniqueID.Replace("$", #"\\$") + "_Div";
Page.ClientScript.RegisterStartupScript(this.GetType(), "CreateSliderFor_" + MyDivName, "$('#" + MyDivName + " > input').slider().on('slideStop', function(slideEvt) { __doPostBack('_TextBox', slideEvt.value); });", true);
}
}
else
{
string MyDivName = this.UniqueID.Replace("$", #"\\$") + "_Div";
Page.ClientScript.RegisterStartupScript(this.GetType(), "CreateSliderFor_" + MyDivName, "$('#" + MyDivName + " > input').slider().on('slideStop', function(slideEvt) { __doPostBack('_TextBox', slideEvt.value); });", true);
}
}
}
}
Usage
Put this at the top of the page, after #Page line:
<%# Register Src="~/Slider.ascx" TagPrefix="uc1" TagName="Slider" %>
Now place Slider control anywhere in the page:
<uc1:Slider runat="server" ID="Slider1" Min="10" Max="24" Value="<%# BindItem.Age %>" Step="0.1" Precision="1" IsRange="true" OnValueChanged="Slider1_ValueChanged" />
The event handler looks like this:
protected void Slider1_ValueChanged()
{
//do whatever you want with Slider1.Value or Slider1.Value2
}

Related

Xamarin forms limit number of decimal places in an entry field

I have an entry field inside of a collection view cell. The entry field should accept numeric value only with certain decimal places. I don't want more input if they exceed the decimal places. The decimal places vary for each cell. How do I create this validation in Model or using behaviour/trigger?
Currently, my code is like this
<Entry Placeholder="Quantity"
Keyboard="Numeric"
Text="{Binding EnteredQuantity, Mode=TwoWay}"/>
And in the model part, I am doing this
private string enteredQuantity = "1";
public string EnteredQuantity
{
get => enteredQuantity;
set
{
if (!decimal.TryParse(value, out decimal parsedQuantity))
{
enteredQuantity = "1";
OnPropertyChanged(enteredQuantity);
}
else
{
if (parsedQuantity > 9999.99M)
{
enteredQuantity =string.Format("9999.99", parsedQuantity);
OnPropertyChanged(enteredQuantity);
}
else
{
string formatString = "{" + "0:0.".PadRight(4 + (int) BaseDecimalPlaces, '#') + "}"; //Format of {0:0.##}
if (IsAlternateUnitUsed && selectedUnit.Value == "A")
{
formatString = "{" + "0:0.".PadRight(4 + (int) AlternateDecimalPlaces, '#') + "}";
}
enteredQuantity = string.Format(formatString, parsedQuantity);
OnPropertyChanged(enteredQuantity);
}
}
}
}
For the above test, the format string is {0:0.##} and decimal places are 2.
I am trying to restrict the value in the Binding property. However, this does not reflect on the front end. The frontend will show any decimal places like in the picture below. Does anybody have an idea how to solve this?
Has
Use behavior to achieve this
Xaml Code
...
xmlns:local="clr-namespace:DummyTestApp"
...
<Entry HorizontalOptions="FillAndExpand" VerticalOptions="CenterAndExpand">
<Entry.Behaviors>
<local:LengthValidateBehavior MaxLength="7"/>
</Entry.Behaviors>
</Entry>
Behavior Class
public class LengthValidateBehavior : Behavior<Entry>
{
public static BindableProperty MaxLengthProperty = BindableProperty.Create(nameof(MaxLength), typeof(int), typeof(LengthValidateBehavior), 5/* default value*/);
public int MaxLength
{
get
{
return (int)GetValue(MaxLengthProperty);
}
set
{
SetValue(MaxLengthProperty, value);
}
}
protected override void OnAttachedTo(Entry entry)
{
entry.TextChanged += OnEntryTextChanged;
base.OnAttachedTo(entry);
}
protected override void OnDetachingFrom(Entry entry)
{
entry.TextChanged -= OnEntryTextChanged;
base.OnDetachingFrom(entry);
}
void OnEntryTextChanged(object sender, TextChangedEventArgs args)
{
if (sender is Entry entry)
{
if (args.NewTextValue.Length > MaxLength)// write your logic here
{
entry.Text = args.OldTextValue;
}
}
}
}
Similarly use your own logic to restrict the entry as per your own requirements.

Server-Side ASP.net controls rendered as HTML TextArea

I have a server-side control which renders as <input type="Something"> or <textarea>. The code is self-explanatory:
public string Namespace
{
get { return nspace; }
set { nspace = value; }
}
public string Model
{
get { return model; }
set { model = value; }
}
public string Text
{
get { return text; }
set { text = value; }
}
public string TextMode
{
get { return textMode; }
set { textMode = value; }
}
public string _Type
{
get { return type; }
set { type = value; }
}
public string Property { get; set; }
protected override void RenderContents(HtmlTextWriter output)
{
output.AddAttribute(HtmlTextWriterAttribute.Id, Property.ToLower());
output.AddAttribute(HtmlTextWriterAttribute.Name, Property.ToLower());
output.AddAttribute(HtmlTextWriterAttribute.Type, _Type);
if(!String.IsNullOrEmpty(Text))
output.AddAttribute(HtmlTextWriterAttribute.Value, Text);
Type modelType = Type.GetType(string.Format("{0}.{1}", Namespace, Model));
PropertyInfo propInfo = modelType.GetProperty(Property);
var attr = propInfo.GetCustomAttribute<RequiredAttribute>(false);
if (attr != null)
{
output.AddAttribute("data-val", "true");
output.AddAttribute("data-val-required", attr.ErrorMessage);
}
//forces styles to be added to the control
this.AddAttributesToRender(output);
if (!String.IsNullOrEmpty(TextMode))
{
output.RenderBeginTag("textarea");
output.RenderEndTag();
}
else
{
output.RenderBeginTag("input");
output.RenderEndTag();
}
}
This control is aimed at getting validation error messages from Data Model (instead of providing "data-val" and "data-val-required" to every textbox).
using this code is easy:
<ServerControlTag:ControlName Property="aProp" runat="Server" Model="MyModel" ID="txtSomething" />
Which renders as a input type=text tag, and the following renders as a textarea tag:
<ServerControlTag:ControlName Property="Description" runat="Server" Model="MyModel" TextMode="MultiLine" ID="txtDescription" class="message" />
My problem is when rendering textarea I cannot find any attribute to fill the text of textarrea. To set text in a textarea I have just found the following syntax:
<textarea ... > My Text Here </textarea>
yet, I don't know how to implement it in my server control. I don't know even if I am on the right track.
You need to call the normal Write() method to write text inside the tag.
Remember to HTML-encode the text.

Mail Sending Program & Request Timeout

I was asked to develop an auto-mail sending program on asp.net. It is supposed to send, say 5000 e-mails reading the addresses from a database. It will sure fall into request timeout tough. So it seems I have to convert it into a windows app. But I'd like to know if ajaxifying this web-app would help. If I write a web service, and my web app sends the mail addresses as lists of 50 at a time. when done, send the next 50 and so on. Would this help to solve the problem of http request timeout?
Using a webservice endpoint to send your emails is a good idea, whether you call it from an aspx class or from the client with javascript.
Simply use the webservice call to spawn a thread to send the emails and return immediately.
If you wanted visual progress cues then write another ajax endpoint or aspx page that will display the status of the email thread's progress.
There are many ways to accomplish this, you should be able to come up with one with the information given.
Batching from ajax is probably going to be more work than you want to do and adds unnecessary complexity (which is never a good thing).
this is interesting. I may spike this and post some code.
Ok, im back. here ya go. both a webform ui and an ajax ui.
None of this is meant to be finished product - is a spike to support a story. bend/fold/spindle at will.
EmailService.asmx
using System;
using System.ComponentModel;
using System.Threading;
using System.Web.Script.Services;
using System.Web.Services;
namespace EmailSendingWebApplication
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
[ScriptService]
public class EmailService : WebService
{
private static EmailSendingProgress _currentProgress;
private static Thread _emailThread;
/// <summary>
///
/// </summary>
/// <param name="criteria">just an example</param>
/// <param name="anotherCriteria">just an example</param>
/// <returns></returns>
[WebMethod]
public EmailSendingProgress SendEmails(string criteria, int anotherCriteria)
{
try
{
if (_currentProgress != null && _emailThread.IsAlive)
{
throw new InvalidOperationException(
"Email batch is already in progress. Wait for completion or cancel");
}
// use your criteria to cue up the emails to be sent.
// .....
// and derive a way for a thread to identify the emails
// i am using a batchid
int batchId = 1000; // contrived
// create a thread
_emailThread = new Thread(ProcessEmails);
_currentProgress = new EmailSendingProgress
{
Status = ProcessState.Starting,
BatchId = batchId
};
// you could use a 'state' object but this process/thread
// is single use/single instance just access _currentProgress
// by the static member
_emailThread.Start();
return _currentProgress;
}
catch (Exception ex)
{
_currentProgress = new EmailSendingProgress
{
Status = ProcessState.Error,
Message = "Error starting process:" + ex.Message
};
}
return _currentProgress;
}
[WebMethod]
public EmailSendingProgress CancelEmailProcess()
{
if (_currentProgress != null && _emailThread.IsAlive)
{
_currentProgress.Cancel = true;
_currentProgress.Message = "Cancelling";
}
return _currentProgress;
}
[WebMethod]
public EmailSendingProgress GetProgress()
{
return _currentProgress;
}
private static void ProcessEmails()
{
// process your emails using the criteria, in this case,
// a batchId
int totalEmails = 100;
int currentEmail = 0;
lock (_currentProgress)
{
_currentProgress.Total = totalEmails;
_currentProgress.Status = ProcessState.Processing;
}
for (; currentEmail < totalEmails; currentEmail++)
{
lock (_currentProgress)
{
if (_currentProgress.Cancel)
{
_currentProgress.Status = ProcessState.Cancelled;
_currentProgress.Message = "User cancelled process.";
break;
}
_currentProgress.Current = currentEmail + 1;
}
try
{
// send your email
Thread.Sleep(100); // lallalala sending email
}
catch (Exception ex)
{
// log the failure in your db
// then check to see if we should exit on error
// or just keep going.
lock (_currentProgress)
{
if (_currentProgress.CancelBatchOnSendError)
{
_currentProgress.Status = ProcessState.Error;
_currentProgress.Message = ex.Message;
break;
}
}
}
}
{
// don't want to obscure state/message from abnormal
// termination..
if (_currentProgress.Status == ProcessState.Processing)
{
_currentProgress.Status = ProcessState.Idle;
_currentProgress.Message = "Processing complete.";
}
}
}
}
public enum ProcessState
{
Idle,
Starting,
Processing,
Cancelled,
Error
}
[Serializable]
public class EmailSendingProgress
{
public int BatchId;
public bool Cancel;
public bool CancelBatchOnSendError;
public int Current;
public string Message;
public ProcessState Status;
public int Total;
}
}
WebFormUI.aspx
<%# Page Language="C#" %>
<%# Import Namespace="EmailSendingWebApplication" %>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
var svc = new EmailService();
UpdateProgress(svc.GetProgress());
}
protected void SendEmailsButton_Click(object sender, EventArgs e)
{
// arbitrary params - modify to suit
string criteria = string.Empty;
int anotherCriteria = 0;
var svc = new EmailService();
UpdateProgress(svc.SendEmails(criteria, anotherCriteria));
}
protected void CancelEmailProcessButton_Click(object sender, EventArgs e)
{
var svc = new EmailService();
UpdateProgress(svc.CancelEmailProcess());
}
private void UpdateProgress(EmailSendingProgress progress)
{
SetButtonState(progress);
DisplayProgress(progress);
}
private void DisplayProgress(EmailSendingProgress progress)
{
if (progress != null)
{
EmailProcessProgressLabel.Text = string.Format("Sending {0} of {1}", progress.Current, progress.Total);
EmailProcessStatusLabel.Text = progress.Status.ToString();
EmailProcessMessageLabel.Text = progress.Message;
}
else
{
EmailProcessProgressLabel.Text = string.Empty;
EmailProcessStatusLabel.Text = string.Empty;
EmailProcessMessageLabel.Text = string.Empty;
}
}
private void SetButtonState(EmailSendingProgress progress)
{
if (progress != null &&
(progress.Status == ProcessState.Starting || progress.Status == ProcessState.Processing))
{
CancelEmailProcessButton.Visible = true;
SendEmailsButton.Visible = false;
}
else
{
CancelEmailProcessButton.Visible = false;
SendEmailsButton.Visible = true;
}
}
protected void RefreshButton_Click(object sender, EventArgs e)
{
// noop just to get postback. you could also use meta headers to refresh the page automatically
// but why?
}
</script>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<br />
EmailProcessStatus:
<asp:Label ID="EmailProcessStatusLabel" runat="server" Text="EmailProcessStatus"></asp:Label>
<br />
EmailProcessProgress:
<asp:Label ID="EmailProcessProgressLabel" runat="server" Text="EmailProcessProgress"></asp:Label>
<br />
EmailProcessMessage:<asp:Label ID="EmailProcessMessageLabel" runat="server" Text="EmailProcessMessage"></asp:Label>
<br />
<br />
<asp:Button ID="SendEmailsButton" runat="server" OnClick="SendEmailsButton_Click"
Text="Send Emails" />
<asp:Button ID="CancelEmailProcessButton" runat="server" OnClick="CancelEmailProcessButton_Click"
Text="Cancel Email Process" />
<br />
<br />
<asp:Button ID="RefreshButton" runat="server" OnClick="RefreshButton_Click" Text="Refresh" />
</div>
</form>
</body>
</html>
AjaxUI.htm
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script type="text/javascript">
//http://www.codeproject.com/Articles/38999/Consuming-ASP-net-WebServices-WCF-Services-and-sta.aspx
var ProcessState = ["Idle", "Starting", "Processing", "Cancelled", "Error"];
function createXHR() {
var xhr;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
}
else if (window.ActiveXObject) {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
else {
throw new Error("Could not create XMLHttpRequest object.");
}
return xhr;
}
function emailAjax(operation, postData, callback) {
var xhr = createXHR();
xhr.open("POST", "EmailService.asmx/" + operation, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
callback(xhr.responseText);
}
};
xhr.setRequestHeader("content-type", "application/json");
xhr.send(postData);
}
function $(id) {
var e = document.getElementById(id);
return e;
}
function startProcess() {
var postData = '{"criteria" : "something", "anotherCriteria" : "1"}';
emailAjax("SendEmails", postData, displayProgress);
}
function cancelProcess() {
emailAjax("CancelEmailProcess", null, displayProgress);
}
function getProgress() {
emailAjax("GetProgress", null, displayProgress);
}
function displayProgress(json) {
eval('var result=' + json + '; var prg=result.d;');
$("EmailProcessMessage").innerHTML = "";
$("EmailProcessStatus").innerHTML = "";
$("EmailProcessProgress").innerHTML = "";
$("CancelEmailProcessButton").style.display = "none";
$("SendEmailsButton").style.display = "none";
if (prg) {
$("EmailProcessMessage").innerHTML = prg.Message;
$("EmailProcessStatus").innerHTML = ProcessState[prg.Status];
$("EmailProcessProgress").innerHTML = "Sending " + prg.Current + " of " + prg.Total;
}
if (prg && (prg.Status == 1 || prg.Status == 2)) {
$("SendEmailsButton").style.display = "none";
$("CancelEmailProcessButton").style.display = "inline";
}
else {
$("CancelEmailProcessButton").style.display = "none";
$("SendEmailsButton").style.display = "inline";
}
}
function init() {
$("SendEmailsButton").onclick = startProcess;
$("CancelEmailProcessButton").onclick = cancelProcess;
// kinda quick but we are only proccing 100 emails for demo
window.setInterval(getProgress, 1000);
}
</script>
</head>
<body onload="init()">
EmailProcessStatus:<span id="EmailProcessStatus"></span><br />
EmailProcessProgress:<span id="EmailProcessProgress"></span><br />
EmailProcessMessage:<span id="EmailProcessMessage"></span><br />
<input type="button" id="SendEmailsButton" value="SendEmails" style="display: none" />
<input type="button" id="CancelEmailProcessButton" value="CancelEmailProcess" style="display: none" />
</body>
</html>
So user will have to leave the browser window open until all the e-mails are sent? Does not sound very good. I would solve this using a daemon or simple script that is run by cron (and checks db if there is something to send), on Windows I hope you can do something similar (write Windows service etc.). This is a purely server-side task, I think ajaxifying it shows that author of the web app wasn't able to make it in a better way, it may even make your web app to be mentioned on thedailywtf.com :)

Hidden Field altered through javascript not persisting on postback

I have a web user control with a hidden field on it. When a javascript event (click) ocurrs, I am trying to set a value in the hidden field, so that the value can be retained on postback and remembered for the next rendering. The control is a collapsible panel extender that does not cause postback, uses jquery, and if postback occurs elsewhere on the page, it remembers if it is expanded or collapsed.
The problem is that the javascript executes, but does not actually change the value in the hidden field. If I use the dom explorer, the hidden fiend is still set to the default, and then when I debug, in the the next postback the hidden field is still set to the default as well.
I have also tried using the tried and true getElementById with no success.
No javascript errors occur.
ASCX code:
<input id="hiddenCurrentState" type="hidden" runat="server" />
Codebehind:
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Text;
public partial class Controls_SubControls_CollapsiblePanelExtender : System.Web.UI.UserControl
{
public string HeaderControlId { get; set; }
public string BodyControlId { get; set; }
public string CollapseAllControlId { get; set; }
public string ShowAllControlId { get; set; }
public CollapsedState DefaultState { get; set; }
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
hiddenCurrentState.Value = DefaultState.ToString();
}
}
protected override void OnPreRender(EventArgs e)
{
BuildJQueryScript();
base.OnPreRender(e);
}
private void BuildJQueryScript()
{
StringBuilder script = new StringBuilder();
script.Append("$(document).ready(function(){\n");
//toggle based on current state
script.Append("if ($(\"#" + hiddenCurrentState.ClientID + "\").attr(\"value\")==\"Expanded\")\n");
script.Append("{\n");
script.Append("$(\"#" + BodyControlId + "\").show();\n");
script.Append("$(\"#" + hiddenCurrentState.ClientID + "\").val(\"Expanded\");\n");
script.Append("}\n");
script.Append("else\n");
script.Append("{\n");
script.Append("$(\"#" + BodyControlId + "\").hide();\n");
script.Append("$(\"#" + hiddenCurrentState.ClientID + "\").val(\"Collapsed\");\n");
script.Append("}\n");
//toggle on click
script.Append("$(\"#" + HeaderControlId + "\").click(function(){\n");
script.Append(" $(this).next(\"#" + BodyControlId + "\").slideToggle(500)\n");
script.Append(" return false;\n");
script.Append("});\n");
//collapse all
if (!String.IsNullOrEmpty(CollapseAllControlId))
{
script.Append("$(\"#" + CollapseAllControlId + "\").click(function(){\n");
script.Append(" $(\"#" + BodyControlId + "\").slideUp(500)\n");
script.Append(" return false;\n");
script.Append("});\n");
}
//show all
if (!String.IsNullOrEmpty(ShowAllControlId))
{
script.Append("$(\"#" + ShowAllControlId + "\").click(function(){\n");
script.Append(" $(this).hide()\n");
script.Append(" $(\"#" + BodyControlId + "\").slideDown()\n");
//script.Append(" $(\".message_list li:gt(4)\").slideDown()\n");
script.Append(" return false;\n");
script.Append("});\n");
}
script.Append("});\n");
Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "CollapsiblePanelScript", script.ToString(), true);
}
}
public enum CollapsedState
{
Expanded = 0,
Collapsed = 1
}
I don't see where you are setting the value of the hidden field on the client side. I would expect to see a line something like the following in your collapse/show functions to actually change the value on the client when the panel is collapsed/expanded.
Collapse:
script.Append( " $(\"#" + hiddenCurrentState.ClientID + "\").val(1);\n" );
Show:
script.Append( " $(\"#" + hiddenCurrentState.ClientID + "\").val(0);\n" );
On every post back the entire page is rendered again, any values changed on the client side will not be persisted.
I recommend you store the state in a cookie. As you are using jQuery, the COOKIE library makes this a cinch.
Have you tried using <asp:HiddenField> instead of <input>?

Is there a workaround for IE 6/7 "Unspecified Error" bug when accessing offsetParent

I'm using jQuery UI's draggable and droppable libraries in a simple ASP.NET proof of concept application. This page uses the ASP.NET AJAX UpdatePanel to do partial page updates. The page allows a user to drop an item into a trashcan div, which will invoke a postback that deletes a record from the database, then rebinds the list (and other controls) that the item was drug from. All of these elements (the draggable items and the trashcan div) are inside an ASP.NET UpdatePanel.
Here is the dragging and dropping initialization script:
function initDragging()
{
$(".person").draggable({helper:'clone'});
$("#trashcan").droppable({
accept: '.person',
tolerance: 'pointer',
hoverClass: 'trashcan-hover',
activeClass: 'trashcan-active',
drop: onTrashCanned
});
}
$(document).ready(function(){
initDragging();
var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_endRequest(function()
{
initDragging();
});
});
function onTrashCanned(e,ui)
{
var id = $('input[id$=hidID]', ui.draggable).val();
if (id != undefined)
{
$('#hidTrashcanID').val(id);
__doPostBack('btnTrashcan','');
}
}
When the page posts back, partially updating the UpdatePanel's content, I rebind the draggables and droppables. When I then grab a draggable with my cursor, I get an "htmlfile: Unspecified error." exception. I can resolve this problem in the jQuery library by replacing elem.offsetParent with calls to this function that I wrote:
function IESafeOffsetParent(elem)
{
try
{
return elem.offsetParent;
}
catch(e)
{
return document.body;
}
}
I also have to avoid calls to elem.getBoundingClientRect() as it throws the same error. For those interested, I only had to make these changes in the jQuery.fn.offset function in the Dimensions Plugin.
My questions are:
Although this works, are there better ways (cleaner; better performance; without having to modify the jQuery library) to solve this problem?
If not, what's the best way to manage keeping my changes in sync when I update the jQuery libraries in the future? For, example can I extend the library somewhere other than just inline in the files that I download from the jQuery website.
Update:
#some It's not publicly accessible, but I will see if SO will let me post the relevant code into this answer. Just create an ASP.NET Web Application (name it DragAndDrop) and create these files. Don't forget to set Complex.aspx as your start page. You'll also need to download the jQuery UI drag and drop plug in as well as jQuery core
Complex.aspx
<%# Page Language="C#" AutoEventWireup="true" CodeBehind="Complex.aspx.cs" Inherits="DragAndDrop.Complex" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
<script src="jquery-1.2.6.min.js" type="text/javascript"></script>
<script src="jquery-ui-personalized-1.5.3.min.js" type="text/javascript"></script>
<script type="text/javascript">
function initDragging()
{
$(".person").draggable({helper:'clone'});
$("#trashcan").droppable({
accept: '.person',
tolerance: 'pointer',
hoverClass: 'trashcan-hover',
activeClass: 'trashcan-active',
drop: onTrashCanned
});
}
$(document).ready(function(){
initDragging();
var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_endRequest(function()
{
initDragging();
});
});
function onTrashCanned(e,ui)
{
var id = $('input[id$=hidID]', ui.draggable).val();
if (id != undefined)
{
$('#hidTrashcanID').val(id);
__doPostBack('btnTrashcan','');
}
}
</script>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<div>
<asp:UpdatePanel ID="updContent" runat="server" UpdateMode="Always">
<ContentTemplate>
<asp:LinkButton ID="btnTrashcan" Text="trashcan" runat="server" CommandName="trashcan"
onclick="btnTrashcan_Click" style="display:none;"></asp:LinkButton>
<input type="hidden" id="hidTrashcanID" runat="server" />
<asp:Button ID="Button1" runat="server" Text="Save" onclick="Button1_Click" />
<table>
<tr>
<td style="width: 300px;">
<asp:DataList ID="lstAllPeople" runat="server" DataSourceID="odsAllPeople"
DataKeyField="ID">
<ItemTemplate>
<div class="person">
<asp:HiddenField ID="hidID" runat="server" Value='<%# Eval("ID") %>' />
Name:
<asp:Label ID="lblName" runat="server" Text='<%# Eval("Name") %>' />
<br />
<br />
</div>
</ItemTemplate>
</asp:DataList>
<asp:ObjectDataSource ID="odsAllPeople" runat="server" SelectMethod="SelectAllPeople"
TypeName="DragAndDrop.Complex+DataAccess"
onselecting="odsAllPeople_Selecting">
<SelectParameters>
<asp:Parameter Name="filter" Type="Object" />
</SelectParameters>
</asp:ObjectDataSource>
</td>
<td style="width: 300px;vertical-align:top;">
<div id="trashcan">
drop here to delete
</div>
<asp:DataList ID="lstPeopleToDelete" runat="server"
DataSourceID="odsPeopleToDelete">
<ItemTemplate>
ID:
<asp:Label ID="IDLabel" runat="server" Text='<%# Eval("ID") %>' />
<br />
Name:
<asp:Label ID="NameLabel" runat="server" Text='<%# Eval("Name") %>' />
<br />
<br />
</ItemTemplate>
</asp:DataList>
<asp:ObjectDataSource ID="odsPeopleToDelete" runat="server"
onselecting="odsPeopleToDelete_Selecting" SelectMethod="GetDeleteList"
TypeName="DragAndDrop.Complex+DataAccess">
<SelectParameters>
<asp:Parameter Name="list" Type="Object" />
</SelectParameters>
</asp:ObjectDataSource>
</td>
</tr>
</table>
</ContentTemplate>
</asp:UpdatePanel>
</div>
</form>
</body>
</html>
Complex.aspx.cs
namespace DragAndDrop
{
public partial class Complex : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
protected List<int> DeleteList
{
get
{
if (ViewState["dl"] == null)
{
List<int> dl = new List<int>();
ViewState["dl"] = dl;
return dl;
}
else
{
return (List<int>)ViewState["dl"];
}
}
}
public class DataAccess
{
public IEnumerable<Person> SelectAllPeople(IEnumerable<int> filter)
{
return Database.SelectAll().Where(p => !filter.Contains(p.ID));
}
public IEnumerable<Person> GetDeleteList(IEnumerable<int> list)
{
return Database.SelectAll().Where(p => list.Contains(p.ID));
}
}
protected void odsAllPeople_Selecting(object sender, ObjectDataSourceSelectingEventArgs e)
{
e.InputParameters["filter"] = this.DeleteList;
}
protected void odsPeopleToDelete_Selecting(object sender, ObjectDataSourceSelectingEventArgs e)
{
e.InputParameters["list"] = this.DeleteList;
}
protected void Button1_Click(object sender, EventArgs e)
{
foreach (int id in DeleteList)
{
Database.DeletePerson(id);
}
DeleteList.Clear();
lstAllPeople.DataBind();
lstPeopleToDelete.DataBind();
}
protected void btnTrashcan_Click(object sender, EventArgs e)
{
int id = int.Parse(hidTrashcanID.Value);
DeleteList.Add(id);
lstAllPeople.DataBind();
lstPeopleToDelete.DataBind();
}
}
}
Database.cs
namespace DragAndDrop
{
public static class Database
{
private static Dictionary<int, Person> _people = new Dictionary<int,Person>();
static Database()
{
Person[] people = new Person[]
{
new Person("Chad")
, new Person("Carrie")
, new Person("Richard")
, new Person("Ron")
};
foreach (Person p in people)
{
_people.Add(p.ID, p);
}
}
public static IEnumerable<Person> SelectAll()
{
return _people.Values;
}
public static void DeletePerson(int id)
{
if (_people.ContainsKey(id))
{
_people.Remove(id);
}
}
public static Person CreatePerson(string name)
{
Person p = new Person(name);
_people.Add(p.ID, p);
return p;
}
}
public class Person
{
private static int _curID = 1;
public int ID { get; set; }
public string Name { get; set; }
public Person()
{
ID = _curID++;
}
public Person(string name)
: this()
{
Name = name;
}
}
}
#arilanto - I include this script after my jquery scripts. Performance wise, it's not the best solution, but it is a quick easy work around.
function IESafeOffsetParent(elem)
{
try
{
return elem.offsetParent;
}
catch(e)
{
return document.body;
}
}
// The Offset Method
// Originally By Brandon Aaron, part of the Dimension Plugin
// http://jquery.com/plugins/project/dimensions
jQuery.fn.offset = function() {
/// <summary>
/// Gets the current offset of the first matched element relative to the viewport.
/// </summary>
/// <returns type="Object">An object with two Integer properties, 'top' and 'left'.</returns>
var left = 0, top = 0, elem = this[0], results;
if ( elem ) with ( jQuery.browser ) {
var parent = elem.parentNode,
offsetChild = elem,
offsetParent = IESafeOffsetParent(elem),
doc = elem.ownerDocument,
safari2 = safari && parseInt(version) < 522 && !/adobeair/i.test(userAgent),
css = jQuery.curCSS,
fixed = css(elem, "position") == "fixed";
// Use getBoundingClientRect if available
if (false && elem.getBoundingClientRect) {
var box = elem.getBoundingClientRect();
// Add the document scroll offsets
add(box.left + Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft),
box.top + Math.max(doc.documentElement.scrollTop, doc.body.scrollTop));
// IE adds the HTML element's border, by default it is medium which is 2px
// IE 6 and 7 quirks mode the border width is overwritable by the following css html { border: 0; }
// IE 7 standards mode, the border is always 2px
// This border/offset is typically represented by the clientLeft and clientTop properties
// However, in IE6 and 7 quirks mode the clientLeft and clientTop properties are not updated when overwriting it via CSS
// Therefore this method will be off by 2px in IE while in quirksmode
add( -doc.documentElement.clientLeft, -doc.documentElement.clientTop );
// Otherwise loop through the offsetParents and parentNodes
} else {
// Initial element offsets
add( elem.offsetLeft, elem.offsetTop );
// Get parent offsets
while ( offsetParent ) {
// Add offsetParent offsets
add( offsetParent.offsetLeft, offsetParent.offsetTop );
// Mozilla and Safari > 2 does not include the border on offset parents
// However Mozilla adds the border for table or table cells
if ( mozilla && !/^t(able|d|h)$/i.test(offsetParent.tagName) || safari && !safari2 )
border( offsetParent );
// Add the document scroll offsets if position is fixed on any offsetParent
if ( !fixed && css(offsetParent, "position") == "fixed" )
fixed = true;
// Set offsetChild to previous offsetParent unless it is the body element
offsetChild = /^body$/i.test(offsetParent.tagName) ? offsetChild : offsetParent;
// Get next offsetParent
offsetParent = offsetParent.offsetParent;
}
// Get parent scroll offsets
while ( parent && parent.tagName && !/^body|html$/i.test(parent.tagName) ) {
// Remove parent scroll UNLESS that parent is inline or a table to work around Opera inline/table scrollLeft/Top bug
if ( !/^inline|table.*$/i.test(css(parent, "display")) )
// Subtract parent scroll offsets
add( -parent.scrollLeft, -parent.scrollTop );
// Mozilla does not add the border for a parent that has overflow != visible
if ( mozilla && css(parent, "overflow") != "visible" )
border( parent );
// Get next parent
parent = parent.parentNode;
}
// Safari <= 2 doubles body offsets with a fixed position element/offsetParent or absolutely positioned offsetChild
// Mozilla doubles body offsets with a non-absolutely positioned offsetChild
if ( (safari2 && (fixed || css(offsetChild, "position") == "absolute")) ||
(mozilla && css(offsetChild, "position") != "absolute") )
add( -doc.body.offsetLeft, -doc.body.offsetTop );
// Add the document scroll offsets if position is fixed
if ( fixed )
add(Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft),
Math.max(doc.documentElement.scrollTop, doc.body.scrollTop));
}
// Return an object with top and left properties
results = { top: top, left: left };
}
function border(elem) {
/// <summary>
/// This method is internal.
/// </summary>
/// <private />
add( jQuery.curCSS(elem, "borderLeftWidth", true), jQuery.curCSS(elem, "borderTopWidth", true) );
}
function add(l, t) {
/// <summary>
/// This method is internal.
/// </summary>
/// <private />
left += parseInt(l, 10) || 0;
top += parseInt(t, 10) || 0;
}
return results;
};
If you would like to fix the minified/compressed .js file for jQuery version 1.4.2, replace:
var d=b.getBoundingClientRect(),
with
var d = null;
try { d = b.getBoundingClientRect(); }
catch(e) { d = { top : b.offsetTop, left : b.offsetLeft } ; }
(note that there is no comma after the closing brace now)
i tried the following workaround for the getBoundingClientRect() unspecified error whilst drag n drop, and it works fine.
in the jquery.1.4.2.js (i.e base jquery file, where the error is thrown exactly)
replace the elem.getBoundingClientRect() function call in js file
//the line which throws the unspecified error
var box = elem.getBoundingClientRect(),
with this..
var box = null;
try
{
box = elem.getBoundingClientRect();
}
catch(e)
{
box = { top : elem.offsetTop, left : elem.offsetLeft } ;
}
This solves the issue and drag n drop will work quitely even after post back through update panel
Regards
Raghu
My version is:
Add function:
function getOffsetSum(elem) {
var top = 0, left = 0
while (elem) {
top = top + parseInt(elem.offsetTop)
left = left + parseInt(elem.offsetLeft)
try {
elem = elem.offsetParent
}
catch (e) {
return { top: top, left: left }
}
}
return { top: top, left: left }
};
replace
var box = this[0].getBoundingClientRect()
with
var box = getOffsetSum(this[0])
PS: jquery-1.3.2.
#Raghu
Thanks for this bit of code! I was having the same problem in IE and this fixed it.
var box = null;
try {
box = elem.getBoundingClientRect();
} catch(e) {
box = {
top : elem.offsetTop,
left : elem.offsetLeft
};
}
This isn't just a jQuery error. I encountered it using ExtJS 4.0.2a on IE8. It seems that IE will almost always stumble on element.getBoundingClientRect() if the element has been replaced in the DOM. Your try/catch hack is pretty much the only way to get around this. I guess the actual solution would be to eventually drop IE < 9 support.
Relevent ExtJS v4.0.2a Source Code (lines 11861-11869):
...
if(el != bd){
hasAbsolute = fly(el).isStyle("position", "absolute");
if (el.getBoundingClientRect) {
b = el.getBoundingClientRect();
scroll = fly(document).getScroll();
ret = [Math.round(b.left + scroll.left), Math.round(b.top + scroll.top)];
} else {
...
With a try/catch fix:
...
if(el != bd){
hasAbsolute = fly(el).isStyle("position", "absolute");
if (el.getBoundingClientRect) {
try {
b = el.getBoundingClientRect();
scroll = fly(document).getScroll();
ret = [Math.round(b.left + scroll.left), Math.round(b.top + scroll.top)];
} catch(e) {
ret = [0,0];
}
} else {
...

Resources