//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
// Certain elements are Copyright (c) 2007 Dominick Baier.
//
//-----------------------------------------------------------------------
[assembly: System.Web.UI.WebResource(DotNetOpenAuth.InfoCard.InfoCardSelector.ScriptResourceName, "text/javascript")]
namespace DotNetOpenAuth.InfoCard {
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Drawing.Design;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Xml;
using DotNetOpenAuth.Messaging;
///
/// The style to use for NOT displaying a hidden region.
///
public enum RenderMode {
///
/// A hidden region should be invisible while still occupying space in the page layout.
///
Static,
///
/// A hidden region should collapse so that it does not occupy space in the page layout.
///
Dynamic
}
///
/// An Information Card selector ASP.NET control.
///
[ParseChildren(true, "ClaimsRequested")]
[PersistChildren(false)]
[DefaultEvent("ReceivedToken")]
[ToolboxData("<{0}:InfoCardSelector runat=\"server\"><{0}:ClaimType Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier\" />
Your browser does not support Information Cards.
{0}:InfoCardSelector>")]
[ContractVerification(true)]
public class InfoCardSelector : CompositeControl, IPostBackEventHandler {
///
/// The resource name for getting at the SupportingScript.js embedded manifest stream.
///
internal const string ScriptResourceName = "DotNetOpenAuth.InfoCard.SupportingScript.js";
#region Property constants
///
/// Default value for the property.
///
private const RenderMode RenderModeDefault = RenderMode.Dynamic;
///
/// Default value for the property.
///
private const bool AutoPostBackDefault = true;
///
/// Default value for the property.
///
private const bool AutoPopupDefault = false;
///
/// Default value for the property.
///
private const string PrivacyUrlDefault = "";
///
/// Default value for the property.
///
private const string PrivacyVersionDefault = "";
///
/// Default value for the property.
///
private const InfoCardImageSize InfoCardImageDefault = InfoCardImage.DefaultImageSize;
///
/// Default value for the property.
///
private const string IssuerPolicyDefault = "";
///
/// Default value for the property.
///
private const string IssuerDefault = WellKnownIssuers.SelfIssued;
///
/// The default value for the property.
///
private const string TokenTypeDefault = "urn:oasis:names:tc:SAML:1.0:assertion";
///
/// The viewstate key for storing the property.
///
private const string IssuerViewStateKey = "Issuer";
///
/// The viewstate key for storing the property.
///
private const string IssuerPolicyViewStateKey = "IssuerPolicy";
///
/// The viewstate key for storing the property.
///
private const string AutoPopupViewStateKey = "AutoPopup";
///
/// The viewstate key for storing the property.
///
private const string ClaimsRequestedViewStateKey = "ClaimsRequested";
///
/// The viewstate key for storing the property.
///
private const string TokenTypeViewStateKey = "TokenType";
///
/// The viewstate key for storing the property.
///
private const string PrivacyUrlViewStateKey = "PrivacyUrl";
///
/// The viewstate key for storing the property.
///
private const string PrivacyVersionViewStateKey = "PrivacyVersion";
///
/// The viewstate key for storing the property.
///
private const string AudienceViewStateKey = "Audience";
///
/// The viewstate key for storing the property.
///
private const string AutoPostBackViewStateKey = "AutoPostBack";
///
/// The viewstate key for storing the property.
///
private const string ImageSizeViewStateKey = "ImageSize";
///
/// The viewstate key for storing the property.
///
private const string RenderModeViewStateKey = "RenderMode";
#endregion
#region Categories
///
/// The "Behavior" property category.
///
private const string BehaviorCategory = "Behavior";
///
/// The "Appearance" property category.
///
private const string AppearanceCategory = "Appearance";
///
/// The "InfoCard" property category.
///
private const string InfoCardCategory = "InfoCard";
#endregion
///
/// The panel containing the controls to display if InfoCard is supported in the user agent.
///
private Panel infoCardSupportedPanel;
///
/// The panel containing the controls to display if InfoCard is NOT supported in the user agent.
///
private Panel infoCardNotSupportedPanel;
///
/// Recalls whether the property has been set yet,
/// so its default can be set as soon as possible without overwriting
/// an intentional value.
///
private bool audienceSet;
///
/// Initializes a new instance of the class.
///
public InfoCardSelector() {
this.ToolTip = InfoCardStrings.SelectorClickPrompt;
Reporting.RecordFeatureUse(this);
}
///
/// Occurs when an InfoCard has been submitted but not decoded yet.
///
[Category(InfoCardCategory)]
public event EventHandler ReceivingToken;
///
/// Occurs when an InfoCard has been submitted and decoded.
///
[Category(InfoCardCategory)]
public event EventHandler ReceivedToken;
///
/// Occurs when an InfoCard token is submitted but an error occurs in processing.
///
[Category(InfoCardCategory)]
public event EventHandler TokenProcessingError;
#region Properties
///
/// Gets the set of claims that are requested from the Information Card.
///
[Description("Specifies the required and optional claims.")]
[PersistenceMode(PersistenceMode.InnerProperty), Category(InfoCardCategory)]
public Collection ClaimsRequested {
get {
Contract.Ensures(Contract.Result>() != null);
if (this.ViewState[ClaimsRequestedViewStateKey] == null) {
var claims = new Collection();
this.ViewState[ClaimsRequestedViewStateKey] = claims;
return claims;
} else {
return (Collection)this.ViewState[ClaimsRequestedViewStateKey];
}
}
}
///
/// Gets or sets the issuer URI.
///
[Description("When receiving managed cards, this is the only Issuer whose cards will be accepted.")]
[Category(InfoCardCategory), DefaultValue(IssuerDefault)]
[TypeConverter(typeof(ComponentModel.IssuersSuggestions))]
public string Issuer {
get { return (string)this.ViewState[IssuerViewStateKey] ?? IssuerDefault; }
set { this.ViewState[IssuerViewStateKey] = value; }
}
///
/// Gets or sets the issuer policy URI.
///
[Description("Specifies the URI of the issuer MEX endpoint")]
[Category(InfoCardCategory), DefaultValue(IssuerPolicyDefault)]
public string IssuerPolicy {
get { return (string)this.ViewState[IssuerPolicyViewStateKey] ?? IssuerPolicyDefault; }
set { this.ViewState[IssuerPolicyViewStateKey] = value; }
}
///
/// Gets or sets the URL to this site's privacy policy.
///
[Description("The URL to this site's privacy policy.")]
[Category(InfoCardCategory), DefaultValue(PrivacyUrlDefault)]
[SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "We construct a Uri to validate the format of the string.")]
[SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "That overload is NOT the same.")]
[SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "This can take ~/ paths.")]
public string PrivacyUrl {
get {
return (string)this.ViewState[PrivacyUrlViewStateKey] ?? PrivacyUrlDefault;
}
set {
ErrorUtilities.VerifyOperation(string.IsNullOrEmpty(value) || this.Page == null || this.DesignMode || (HttpContext.Current != null && HttpContext.Current.Request != null), MessagingStrings.HttpContextRequired);
if (!string.IsNullOrEmpty(value)) {
if (this.Page != null && !this.DesignMode) {
// Validate new value by trying to construct a Uri based on it.
new Uri(new HttpRequestInfo(HttpContext.Current.Request).UrlBeforeRewriting, this.Page.ResolveUrl(value)); // throws an exception on failure.
} else {
// We can't fully test it, but it should start with either ~/ or a protocol.
if (Regex.IsMatch(value, @"^https?://")) {
new Uri(value); // make sure it's fully-qualified, but ignore wildcards
} else if (value.StartsWith("~/", StringComparison.Ordinal)) {
// this is valid too
} else {
throw new UriFormatException();
}
}
}
this.ViewState[PrivacyUrlViewStateKey] = value;
}
}
///
/// Gets or sets the version of the privacy policy file.
///
[Description("Specifies the version of the privacy policy file")]
[Category(InfoCardCategory), DefaultValue(PrivacyVersionDefault)]
public string PrivacyVersion {
get { return (string)this.ViewState[PrivacyVersionViewStateKey] ?? PrivacyVersionDefault; }
set { this.ViewState[PrivacyVersionViewStateKey] = value; }
}
///
/// Gets or sets the URI that must be found for the SAML token's intended audience
/// in order for the token to be processed.
///
/// Typically the URI of the page hosting the control, or null to disable audience verification.
///
/// Disabling audience verification introduces a security risk
/// because tokens can be redirected to allow access to unintended resources.
///
[Description("Specifies the URI that must be found for the SAML token's intended audience.")]
[Bindable(true), Category(InfoCardCategory)]
[TypeConverter(typeof(ComponentModel.UriConverter))]
[UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
public Uri Audience {
get {
return (Uri)this.ViewState[AudienceViewStateKey];
}
set {
this.ViewState[AudienceViewStateKey] = value;
this.audienceSet = true;
}
}
///
/// Gets or sets a value indicating whether a postback will automatically
/// be invoked when the user selects an Information Card.
///
[Description("Specifies if the pages automatically posts back after the user has selected a card")]
[Category(BehaviorCategory), DefaultValue(AutoPostBackDefault)]
public bool AutoPostBack {
get { return (bool)(this.ViewState[AutoPostBackViewStateKey] ?? AutoPostBackDefault); }
set { this.ViewState[AutoPostBackViewStateKey] = value; }
}
///
/// Gets or sets the size of the standard InfoCard image to display.
///
/// The default size is 114x80.
[Description("The size of the InfoCard image to use. Defaults to 114x80.")]
[DefaultValue(InfoCardImageDefault), Category(AppearanceCategory)]
public InfoCardImageSize ImageSize {
get { return (InfoCardImageSize)(this.ViewState[ImageSizeViewStateKey] ?? InfoCardImageDefault); }
set { this.ViewState[ImageSizeViewStateKey] = value; }
}
///
/// Gets or sets the template to display when the user agent lacks
/// an Information Card selector.
///
[Browsable(false), DefaultValue("")]
[PersistenceMode(PersistenceMode.InnerProperty), TemplateContainer(typeof(InfoCardSelector))]
public virtual ITemplate UnsupportedTemplate { get; set; }
///
/// Gets or sets a value indicating whether a hidden region (either
/// the unsupported or supported InfoCard HTML)
/// collapses or merely becomes invisible when it is not to be displayed.
///
[Description("Whether the hidden region collapses or merely becomes invisible.")]
[Category(AppearanceCategory), DefaultValue(RenderModeDefault)]
public RenderMode RenderMode {
get { return (RenderMode)(this.ViewState[RenderModeViewStateKey] ?? RenderModeDefault); }
set { this.ViewState[RenderModeViewStateKey] = value; }
}
///
/// Gets or sets a value indicating whether the identity selector will be triggered at page load.
///
[Description("Controls whether the InfoCard selector automatically appears when the page is loaded.")]
[Category(BehaviorCategory), DefaultValue(AutoPopupDefault)]
public bool AutoPopup {
get { return (bool)(this.ViewState[AutoPopupViewStateKey] ?? AutoPopupDefault); }
set { this.ViewState[AutoPopupViewStateKey] = value; }
}
#endregion
///
/// Gets the name of the hidden field that is used to transport the token back to the server.
///
private string HiddenFieldName {
get { return this.ClientID + "_tokenxml"; }
}
///
/// Gets the id of the OBJECT tag that creates the InfoCard Selector.
///
private string SelectorObjectId {
get { return this.ClientID + "_cs"; }
}
///
/// Gets the XML token, which will be encrypted if it was received over SSL.
///
private string TokenXml {
get { return this.Page.Request.Form[this.HiddenFieldName]; }
}
///
/// Gets or sets the type of token the page is prepared to receive.
///
[Description("Specifies the token type. Defaults to SAML 1.0")]
[DefaultValue(TokenTypeDefault), Category(InfoCardCategory)]
private string TokenType {
get { return (string)this.ViewState[TokenTypeViewStateKey] ?? TokenTypeDefault; }
set { this.ViewState[TokenTypeViewStateKey] = value; }
}
///
/// When implemented by a class, enables a server control to process an event raised when a form is posted to the server.
///
/// A that represents an optional event argument to be passed to the event handler.
void IPostBackEventHandler.RaisePostBackEvent(string eventArgument) {
this.RaisePostBackEvent(eventArgument);
}
///
/// When implemented by a class, enables a server control to process an event raised when a form is posted to the server.
///
/// A that represents an optional event argument to be passed to the event handler.
[SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "Predefined signature.")]
protected virtual void RaisePostBackEvent(string eventArgument) {
if (!string.IsNullOrEmpty(this.TokenXml)) {
try {
ReceivingTokenEventArgs receivingArgs = this.OnReceivingToken(this.TokenXml);
if (!receivingArgs.Cancel) {
try {
Token token = Token.Read(this.TokenXml, this.Audience, receivingArgs.DecryptingTokens);
this.OnReceivedToken(token);
} catch (InformationCardException ex) {
this.OnTokenProcessingError(this.TokenXml, ex);
}
}
} catch (XmlException ex) {
this.OnTokenProcessingError(this.TokenXml, ex);
}
}
}
///
/// Fires the event.
///
/// The token XML, prior to any processing.
/// The event arguments sent to the event handlers.
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "decryptor", Justification = "By design")]
protected virtual ReceivingTokenEventArgs OnReceivingToken(string tokenXml) {
Requires.NotNull(tokenXml, "tokenXml");
var args = new ReceivingTokenEventArgs(tokenXml);
var receivingToken = this.ReceivingToken;
if (receivingToken != null) {
receivingToken(this, args);
}
return args;
}
///
/// Fires the event.
///
/// The token, if it was decrypted.
protected virtual void OnReceivedToken(Token token) {
Requires.NotNull(token, "token");
var receivedInfoCard = this.ReceivedToken;
if (receivedInfoCard != null) {
receivedInfoCard(this, new ReceivedTokenEventArgs(token));
}
}
///
/// Fires the event.
///
/// The unprocessed token.
/// The exception generated while processing the token.
protected virtual void OnTokenProcessingError(string unprocessedToken, Exception ex) {
Requires.NotNull(unprocessedToken, "unprocessedToken");
Requires.NotNull(ex, "ex");
var tokenProcessingError = this.TokenProcessingError;
if (tokenProcessingError != null) {
TokenProcessingErrorEventArgs args = new TokenProcessingErrorEventArgs(unprocessedToken, ex);
tokenProcessingError(this, args);
}
}
///
/// Raises the event.
///
/// An object that contains the event data.
protected override void OnInit(EventArgs e) {
// Give a default for the Audience property that allows for
// the aspx page to have preset it, and ViewState
// to initialize it (even to null) after this.
if (!this.audienceSet && !this.DesignMode) {
this.Audience = this.Page.Request.Url;
}
base.OnInit(e);
this.Page.LoadComplete += delegate { this.EnsureChildControls(); };
}
///
/// Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparation for posting back or rendering.
///
protected override void CreateChildControls() {
base.CreateChildControls();
this.Page.ClientScript.RegisterHiddenField(this.HiddenFieldName, string.Empty);
this.Controls.Add(this.infoCardSupportedPanel = this.CreateInfoCardSupportedPanel());
this.Controls.Add(this.infoCardNotSupportedPanel = this.CreateInfoCardUnsupportedPanel());
this.RenderSupportingScript();
}
///
/// Raises the event.
///
/// An object that contains the event data.
protected override void OnPreRender(EventArgs e) {
base.OnPreRender(e);
if (!this.DesignMode) {
// The Cardspace selector will display an ugly error to the user if
// the privacy URL is present but the privacy version is not.
ErrorUtilities.VerifyOperation(string.IsNullOrEmpty(this.PrivacyUrl) || !string.IsNullOrEmpty(this.PrivacyVersion), InfoCardStrings.PrivacyVersionRequiredWithPrivacyUrl);
}
this.RegisterInfoCardSelectorObjectScript();
}
///
/// Creates a control that renders to <Param Name="{0}" Value="{1}" />
///
/// The parameter name.
/// The parameter value.
/// The control that renders to the Param tag.
private static string CreateParamJs(string name, string value) {
Contract.Ensures(Contract.Result() != null);
string scriptFormat = @" objp = document.createElement('param');
objp.name = {0};
objp.value = {1};
obj.appendChild(objp);
";
return string.Format(
CultureInfo.InvariantCulture,
scriptFormat,
MessagingUtilities.GetSafeJavascriptValue(name),
MessagingUtilities.GetSafeJavascriptValue(value));
}
///
/// Creates the panel whose contents are displayed to the user
/// on a user agent that has an Information Card selector.
///
/// The Panel control
[Pure]
private Panel CreateInfoCardSupportedPanel() {
Contract.Ensures(Contract.Result() != null);
Panel supportedPanel = new Panel();
try {
if (!this.DesignMode) {
// At the user agent, assume InfoCard is not supported until
// the JavaScript discovers otherwise and reveals this panel.
supportedPanel.Style[HtmlTextWriterStyle.Display] = "none";
}
supportedPanel.Controls.Add(this.CreateInfoCardImage());
// trigger the selector at page load?
if (this.AutoPopup && !this.Page.IsPostBack) {
this.Page.ClientScript.RegisterStartupScript(
typeof(InfoCardSelector),
"selector_load_trigger",
this.GetInfoCardSelectorActivationScript(true),
true);
}
return supportedPanel;
} catch {
supportedPanel.Dispose();
throw;
}
}
///
/// Gets the InfoCard selector activation script.
///
/// Whether a postback should always immediately follow the selector, even if is false.
/// The javascript to inject into the surrounding context.
private string GetInfoCardSelectorActivationScript(bool alwaysPostback) {
// generate call do __doPostback
PostBackOptions options = new PostBackOptions(this);
string postback = string.Empty;
if (alwaysPostback || this.AutoPostBack) {
postback = this.Page.ClientScript.GetPostBackEventReference(options) + ";";
}
// generate the onclick script for the image
string invokeScript = string.Format(
CultureInfo.InvariantCulture,
@"if (document.infoCard.activate('{0}', '{1}')) {{ {2} }}",
this.SelectorObjectId,
this.HiddenFieldName,
postback);
return invokeScript;
}
///
/// Creates the panel whose contents are displayed to the user
/// on a user agent that does not have an Information Card selector.
///
/// The Panel control.
[Pure]
private Panel CreateInfoCardUnsupportedPanel() {
Contract.Ensures(Contract.Result() != null);
Panel unsupportedPanel = new Panel();
try {
if (this.UnsupportedTemplate != null) {
this.UnsupportedTemplate.InstantiateIn(unsupportedPanel);
}
return unsupportedPanel;
} catch {
unsupportedPanel.Dispose();
throw;
}
}
///
/// Adds the javascript that adds the info card selector <object> HTML tag to the page.
///
[Pure]
private void RegisterInfoCardSelectorObjectScript() {
string scriptFormat = @"{{
var obj = document.createElement('object');
obj.type = 'application/x-informationcard';
obj.id = {0};
obj.style.display = 'none';
";
StringBuilder script = new StringBuilder();
script.AppendFormat(
CultureInfo.InvariantCulture,
scriptFormat,
MessagingUtilities.GetSafeJavascriptValue(this.ClientID + "_cs"));
if (!string.IsNullOrEmpty(this.Issuer)) {
script.AppendLine(CreateParamJs("issuer", this.Issuer));
}
if (!string.IsNullOrEmpty(this.IssuerPolicy)) {
script.AppendLine(CreateParamJs("issuerPolicy", this.IssuerPolicy));
}
if (!string.IsNullOrEmpty(this.TokenType)) {
script.AppendLine(CreateParamJs("tokenType", this.TokenType));
}
string requiredClaims, optionalClaims;
this.GetRequestedClaims(out requiredClaims, out optionalClaims);
ErrorUtilities.VerifyArgument(!string.IsNullOrEmpty(requiredClaims) || !string.IsNullOrEmpty(optionalClaims), InfoCardStrings.EmptyClaimListNotAllowed);
if (!string.IsNullOrEmpty(requiredClaims)) {
script.AppendLine(CreateParamJs("requiredClaims", requiredClaims));
}
if (!string.IsNullOrEmpty(optionalClaims)) {
script.AppendLine(CreateParamJs("optionalClaims", optionalClaims));
}
if (!string.IsNullOrEmpty(this.PrivacyUrl)) {
string privacyUrl = this.DesignMode ? this.PrivacyUrl : new Uri(Page.Request.Url, Page.ResolveUrl(this.PrivacyUrl)).AbsoluteUri;
script.AppendLine(CreateParamJs("privacyUrl", privacyUrl));
}
if (!string.IsNullOrEmpty(this.PrivacyVersion)) {
script.AppendLine(CreateParamJs("privacyVersion", this.PrivacyVersion));
}
script.AppendLine(@"if (document.infoCard.isSupported()) { document.write(obj.outerHTML); }
}");
this.Page.ClientScript.RegisterClientScriptBlock(typeof(InfoCardSelector), this.ClientID + "tag", script.ToString(), true);
}
///
/// Creates the info card clickable image.
///
/// An Image object.
[Pure]
private Image CreateInfoCardImage() {
// add clickable image
Image image = new Image();
try {
image.ImageUrl = this.Page.ClientScript.GetWebResourceUrl(typeof(InfoCardSelector), InfoCardImage.GetImageManifestResourceStreamName(this.ImageSize));
image.AlternateText = InfoCardStrings.SelectorClickPrompt;
image.ToolTip = this.ToolTip;
image.Style[HtmlTextWriterStyle.Cursor] = "hand";
image.Attributes["onclick"] = this.GetInfoCardSelectorActivationScript(false);
return image;
} catch {
image.Dispose();
throw;
}
}
///
/// Compiles lists of requested/required claims that should accompany
/// any submitted Information Card.
///
/// A space-delimited list of claim type URIs for claims that must be included in a submitted Information Card.
/// A space-delimited list of claim type URIs for claims that may optionally be included in a submitted Information Card.
[Pure]
private void GetRequestedClaims(out string required, out string optional) {
Requires.ValidState(this.ClaimsRequested != null);
Contract.Ensures(Contract.ValueAtReturn(out required) != null);
Contract.Ensures(Contract.ValueAtReturn(out optional) != null);
var nonEmptyClaimTypes = this.ClaimsRequested.Where(c => c.Name != null);
var optionalClaims = from claim in nonEmptyClaimTypes
where claim.IsOptional
select claim.Name;
var requiredClaims = from claim in nonEmptyClaimTypes
where !claim.IsOptional
select claim.Name;
string[] requiredClaimsArray = requiredClaims.ToArray();
string[] optionalClaimsArray = optionalClaims.ToArray();
required = string.Join(" ", requiredClaimsArray);
optional = string.Join(" ", optionalClaimsArray);
Contract.Assume(required != null);
Contract.Assume(optional != null);
}
///
/// Adds Javascript snippets to the page to help the Information Card selector do its work,
/// or to downgrade gracefully if the user agent lacks an Information Card selector.
///
private void RenderSupportingScript() {
Requires.ValidState(this.infoCardSupportedPanel != null);
this.Page.ClientScript.RegisterClientScriptResource(typeof(InfoCardSelector), ScriptResourceName);
if (this.RenderMode == RenderMode.Static) {
this.Page.ClientScript.RegisterStartupScript(
typeof(InfoCardSelector),
"SelectorSupportingScript_" + this.ClientID,
string.Format(CultureInfo.InvariantCulture, "document.infoCard.checkStatic('{0}', '{1}');", this.infoCardSupportedPanel.ClientID, this.infoCardNotSupportedPanel.ClientID),
true);
} else if (RenderMode == RenderMode.Dynamic) {
this.Page.ClientScript.RegisterStartupScript(
typeof(InfoCardSelector),
"SelectorSupportingScript_" + this.ClientID,
string.Format(CultureInfo.InvariantCulture, "document.infoCard.checkDynamic('{0}', '{1}');", this.infoCardSupportedPanel.ClientID, this.infoCardNotSupportedPanel.ClientID),
true);
}
}
}
}