using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.WebControls;
using System.Web.UI;
using System.Collections;
using System.Threading;
using System.ComponentModel;
using System.Web;
using System.Globalization;
using System.Diagnostics;
namespace Symber.Web.APX
{
[ParseChildren(true)]
[PersistChildren(true)]
public class APXAccordion : WebControl
{
#region [ Const Fields ]
internal const string ItemCountViewStateKey = "_!ItemCount";
#endregion
#region [ Fields ]
private APXAccordionBehavior _behavior;
private APXAccordionPaneCollection _panes;
#endregion
#region [ DataBinding Fields ]
private object _dataSource;
private ITemplate _headerTemplate;
private ITemplate _contentTemplate;
private bool _initialized;
private bool _pagePreLoadFired;
private bool _requiresDataBinding;
private bool _throwOnDataPropertyChange;
private DataSourceView _currentView;
private bool _currentViewIsFromDataSourceID;
private bool _currentViewValid;
private DataSourceSelectArguments _arguments;
IEnumerable _selectResult;
EventWaitHandle _selectWait;
#endregion
#region [ Constructors ]
public APXAccordion()
: base(HtmlTextWriterTag.Div)
{
}
#endregion
#region [ Properties ]
[Browsable(true)]
[Category("Behavior")]
[Description("Length of the transition animation in milliseconds")]
[DefaultValue(500)]
public int TransitionDuration
{
get { return AccordionBehavior.TransitionDuration; }
set { AccordionBehavior.TransitionDuration = value; }
}
[Browsable(true)]
[Category("Behavior")]
[Description("Number of frames per second used in the transition animation")]
[DefaultValue(15)]
public int FramesPerSecond
{
get { return AccordionBehavior.FramesPerSecond; }
set { AccordionBehavior.FramesPerSecond = value; }
}
[Browsable(true)]
[Category("Behavior")]
[Description("Whether or not to use a fade effect in the transition animations")]
[DefaultValue(false)]
public bool FadeTransitions
{
get { return AccordionBehavior.FadeTransitions; }
set { AccordionBehavior.FadeTransitions = value; }
}
[Browsable(true)]
[Category("Appearance")]
[Description("Default CSS class for Accordion Pane Headers")]
public string HeaderCssClass
{
get { return AccordionBehavior.HeaderCssClass; }
set { AccordionBehavior.HeaderCssClass = value; }
}
[Browsable(true)]
[Category("Appearance")]
[Description("Default CSS class for the selected Accordion Pane Headers")]
public string HeaderSelectedCssClass
{
get { return AccordionBehavior.HeaderSelectedCssClass; }
set { AccordionBehavior.HeaderSelectedCssClass = value; }
}
[Browsable(true)]
[Category("Appearance")]
[Description("Default CSS class for Accordion Pane Content")]
public string ContentCssClass
{
get { return AccordionBehavior.ContentCssClass; }
set { AccordionBehavior.ContentCssClass = value; }
}
[Browsable(true)]
[Category("Behavior")]
[Description("Determine how the growth of the Accordion will be controlled")]
[DefaultValue(APXAccordionAutoSize.None)]
public APXAccordionAutoSize AutoSize
{
get { return AccordionBehavior.AutoSize; }
set { AccordionBehavior.AutoSize = value; }
}
[Browsable(true)]
[Category("Behavior")]
[Description("Index of the AccordionPane to be displayed")]
[DefaultValue(0)]
public int SelectedIndex
{
get { return AccordionBehavior.SelectedIndex; }
set { AccordionBehavior.SelectedIndex = value; }
}
[Browsable(true)]
[Category("Behavior")]
[Description("Whether or not clicking the header will close the currently opened pane (leaving all the Accordion's panes closed)")]
[DefaultValue(true)]
public bool RequireOpenedPane
{
get { return AccordionBehavior.RequireOpenedPane; }
set { AccordionBehavior.RequireOpenedPane = value; }
}
[Browsable(true)]
[Category("Behavior")]
[Description("Whether or not we suppress the client-side click handlers of any elements in the header sections")]
[DefaultValue(false)]
public bool SuppressHeaderPostbacks
{
get { return AccordionBehavior.SuppressHeaderPostbacks; }
set { AccordionBehavior.SuppressHeaderPostbacks = value; }
}
[Browsable(true)]
[Category("Behavior")]
[Description("Cookie name of select index")]
public string CookieName
{
get { return AccordionBehavior.CookieName; }
set { AccordionBehavior.CookieName = value; }
}
[Browsable(true)]
[Category("Behavior")]
[Description("Cookie Path")]
public string CookiePath
{
get { return AccordionBehavior.CookiePath; }
set { AccordionBehavior.CookiePath = value; }
}
[Browsable(true)]
[Category("Behavior")]
[Description("Cookie enabled")]
public bool CookieEnabled
{
get { return AccordionBehavior.CookieEnabled; }
set { AccordionBehavior.CookieEnabled = value; }
}
[PersistenceMode(PersistenceMode.InnerProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public APXAccordionPaneCollection Panes
{
get
{
if (_panes == null)
_panes = new APXAccordionPaneCollection(this);
return _panes;
}
}
#endregion
#region [ DataBinding Properties ]
[Browsable(false)]
[DefaultValue(null)]
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(APXAccordionContentPanel))]
public virtual ITemplate HeaderTemplate
{
get { return _headerTemplate; }
set { _headerTemplate = value; }
}
[Browsable(false)]
[DefaultValue(null)]
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(APXAccordionContentPanel))]
public virtual ITemplate ContentTemplate
{
get { return _contentTemplate; }
set { _contentTemplate = value; }
}
[Bindable(true)]
[Category("Data")]
[DefaultValue(null)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public virtual object DataSource
{
get { return _dataSource; }
set
{
if ((value == null) || (value is IListSource) || (value is IEnumerable))
{
_dataSource = value;
OnDataPropertyChanged();
}
else
{
throw new ArgumentException("Can't bind to value that is not an IListSource or an IEnumerable.");
}
}
}
[DefaultValue("")]
[IDReferenceProperty(typeof(DataSourceControl))]
[Category("Data")]
public virtual string DataSourceID
{
get { return ViewState["DataSourceID"] as string ?? string.Empty; }
set
{
ViewState["DataSourceID"] = value;
OnDataPropertyChanged();
}
}
[DefaultValue("")]
[Category("Data")]
public virtual string DataMember
{
get { return ViewState["DataMember"] as string ?? string.Empty; }
set
{
ViewState["DataMember"] = value;
OnDataPropertyChanged();
}
}
protected bool IsBoundUsingDataSourceID
{
get { return !string.IsNullOrEmpty(DataSourceID); }
}
protected bool RequiresDataBinding
{
get { return _requiresDataBinding; }
set { _requiresDataBinding = value; }
}
protected DataSourceSelectArguments SelectArguments
{
get
{
if (_arguments == null)
_arguments = CreateDataSourceSelectArguments();
return _arguments;
}
}
#endregion
#region [ Events ]
public event EventHandler<APXAccordionItemEventArgs> ItemCreated;
public event EventHandler<APXAccordionItemEventArgs> ItemDataBound;
public event CommandEventHandler ItemCommand;
#endregion
#region [ DataBinding Methods ]
private DataSourceView ConnectToDataSourceView()
{
// If the current view is correct, there is no need to reconnect
if (_currentViewValid && !DesignMode)
return _currentView;
// Disconnect from old view, if necessary
if ((_currentView != null) && (_currentViewIsFromDataSourceID))
{
// We only care about this event if we are bound through the DataSourceID property
_currentView.DataSourceViewChanged -= new EventHandler(OnDataSourceViewChanged);
}
// Connect to new view
IDataSource ds = null;
string dataSourceID = DataSourceID;
if (!string.IsNullOrEmpty(dataSourceID))
{
// Try to find a DataSource control with the ID specified in DataSourceID
Control control = NamingContainer.FindControl(dataSourceID);
if (control == null)
throw new HttpException(String.Format(CultureInfo.CurrentCulture, "DataSource '{1}' for control '{0}' doesn't exist", ID, dataSourceID));
ds = control as IDataSource;
if (ds == null)
throw new HttpException(String.Format(CultureInfo.CurrentCulture, "'{1}' is not a data source for control '{0}'.", ID, dataSourceID));
}
if (ds == null)
{
// DataSource control was not found, construct a temporary data source to wrap the data
return null;
}
else
{
// Ensure that both DataSourceID as well as DataSource are not set at the same time
if (DataSource != null)
throw new InvalidOperationException("DataSourceID and DataSource can't be set at the same time.");
}
// IDataSource was found, extract the appropriate view and return it
DataSourceView newView = ds.GetView(DataMember);
if (newView == null)
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "DataSourceView not found for control '{0}'", ID));
_currentViewIsFromDataSourceID = IsBoundUsingDataSourceID;
_currentView = newView;
// If we're bound through the DataSourceID proeprty, then we care about this event
if ((_currentView != null) && (_currentViewIsFromDataSourceID))
_currentView.DataSourceViewChanged += new EventHandler(OnDataSourceViewChanged);
_currentViewValid = true;
return _currentView;
}
public override void DataBind()
{
// Don't databind to a data source control when the control is in the designer but not top-level
if (IsBoundUsingDataSourceID && DesignMode && (Site == null))
return;
// do our own databinding
RequiresDataBinding = false;
OnDataBinding(EventArgs.Empty);
}
protected override void OnDataBinding(EventArgs e)
{
base.OnDataBinding(e);
//Only bind if the control has the DataSource or DataSourceID set
if (this.DataSource != null || IsBoundUsingDataSourceID)
{
// reset the control state
ClearPanes();
ClearChildViewState();
// and then create the control hierarchy using the datasource
CreateControlHierarchy(true);
ChildControlsCreated = true;
}
}
protected virtual void CreateControlHierarchy(bool useDataSource)
{
int count = -1;
IEnumerable dataSource = null;
List<APXAccordionPane> itemsArray = new List<APXAccordionPane>();
if (!useDataSource)
{
object viewCount = ViewState[ItemCountViewStateKey];
// ViewState must have a non-null value for ItemCount because we check for
// this in CreateChildControls
if (viewCount != null)
{
count = (int)viewCount;
if (count != -1)
{
List<object> dummyList = new List<object>(count);
for (int i = 0; i < count; i++)
dummyList.Add(null);
dataSource = dummyList;
itemsArray.Capacity = count;
}
}
}
else
{
dataSource = GetData();
count = 0;
ICollection collection = dataSource as ICollection;
if (collection != null)
itemsArray.Capacity = collection.Count;
}
if (dataSource != null)
{
int index = 0;
foreach (object dataItem in dataSource)
{
APXAccordionPane ap = new APXAccordionPane();
Controls.Add(ap);
CreateItem(dataItem, index, APXAccordionItemType.Header, ap.HeaderContainer, HeaderTemplate, useDataSource);
CreateItem(dataItem, index, APXAccordionItemType.Content, ap.ContentContainer, ContentTemplate, useDataSource);
itemsArray.Add(ap);
count++;
index++;
}
}
// If we're binding, save the number of items contained in the repeater for use in round-trips
if (useDataSource)
ViewState[ItemCountViewStateKey] = ((dataSource != null) ? count : -1);
}
private void CreateItem(object dataItem, int index, APXAccordionItemType itemType, APXAccordionContentPanel container, ITemplate template, bool dataBind)
{
if (template == null)
return;
APXAccordionItemEventArgs itemArgs = new APXAccordionItemEventArgs(container, itemType);
OnItemCreated(itemArgs);
container.SetDataItemProperties(dataItem, index, itemType);
template.InstantiateIn(container);
if (dataBind)
{
container.DataBind();
OnItemDataBound(itemArgs);
}
}
protected void EnsureDataBound()
{
try
{
_throwOnDataPropertyChange = true;
if (RequiresDataBinding && !string.IsNullOrEmpty(DataSourceID))
DataBind();
}
finally
{
_throwOnDataPropertyChange = false;
}
}
protected virtual IEnumerable GetData()
{
_selectResult = null;
DataSourceView view = ConnectToDataSourceView();
if (view != null)
{
Debug.Assert(_currentViewValid);
// create a handle here to make sure this is a synchronous operation.
_selectWait = new EventWaitHandle(false, EventResetMode.AutoReset);
view.Select(SelectArguments, new DataSourceViewSelectCallback(DoSelect));
_selectWait.WaitOne();
}
else if (DataSource != null)
{
_selectResult = DataSource as IEnumerable;
}
return _selectResult;
}
protected virtual DataSourceSelectArguments CreateDataSourceSelectArguments()
{
return DataSourceSelectArguments.Empty;
}
private void DoSelect(IEnumerable data)
{
_selectResult = data;
_selectWait.Set();
}
protected override bool OnBubbleEvent(object source, EventArgs args)
{
bool handled = false;
APXAccordionCommandEventArgs accordionArgs = args as APXAccordionCommandEventArgs;
if (accordionArgs != null)
{
OnItemCommand(accordionArgs);
handled = true;
}
return handled;
}
protected virtual void OnDataPropertyChanged()
{
if (_throwOnDataPropertyChange)
throw new HttpException("Invalid data property change");
if (_initialized)
RequiresDataBinding = true;
_currentViewValid = false;
}
protected virtual void OnDataSourceViewChanged(object sender, EventArgs args)
{
RequiresDataBinding = true;
}
protected virtual void OnItemCommand(APXAccordionCommandEventArgs args)
{
if (ItemCommand != null)
ItemCommand(this, args);
}
protected virtual void OnItemCreated(APXAccordionItemEventArgs args)
{
if (ItemCreated != null)
ItemCreated(this, args);
}
protected virtual void OnItemDataBound(APXAccordionItemEventArgs args)
{
if (ItemDataBound != null)
ItemDataBound(this, args);
}
#endregion
#region [ Internal Methods ]
internal void ClearPanes()
{
for (int i = Controls.Count - 1; i >= 0; i--)
if (Controls[i] is APXAccordionPane)
Controls.RemoveAt(i);
}
#endregion
#region [ Private Properties ]
private APXAccordionBehavior AccordionBehavior
{
get
{
if (_behavior == null)
{
// Create the extender
_behavior = new APXAccordionBehavior();
_behavior.ID = ID + "_AccordionBehavior";
_behavior.TargetControlID = ID;
if (!this.DesignMode)
Controls.AddAt(0, _behavior);
}
return _behavior;
}
}
#endregion
#region [ Private Methods ]
private void OnPagePreLoad(object sender, EventArgs e)
{
_initialized = true;
if (Page != null)
{
Page.PreLoad -= new EventHandler(this.OnPagePreLoad);
// Setting RequiresDataBinding to true in OnLoad is too late because the OnLoad page event
// happens before the control.OnLoad method gets called. So a page_load handler on the page
// that calls DataBind won't prevent DataBind from getting called again in PreRender.
if (!Page.IsPostBack)
RequiresDataBinding = true;
// If this is a postback and viewstate is enabled, but we have never bound the control
// before, it is probably because its visibility was changed in the postback. In this
// case, we need to bind the control or it will never appear. This is a common scenario
// for Wizard and MultiView.
if (Page.IsPostBack && IsViewStateEnabled && ViewState[ItemCountViewStateKey] == null)
RequiresDataBinding = true;
_pagePreLoadFired = true;
}
}
#endregion
#region [ Override Implementation of WebControl ]
[EditorBrowsable(EditorBrowsableState.Never)]
public override ControlCollection Controls
{
get { return base.Controls; }
}
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (Page != null)
{
Page.PreLoad += new EventHandler(this.OnPagePreLoad);
if (!IsViewStateEnabled && Page.IsPostBack)
RequiresDataBinding = true;
}
}
protected override void OnLoad(EventArgs e)
{
_initialized = true;
ConnectToDataSourceView();
if (Page != null && !_pagePreLoadFired && ViewState[ItemCountViewStateKey] == null)
{
// If the control was added after PagePreLoad, we still need to databind it because it missed its
// first change in PagePreLoad. If this control was created by a call to a parent control's DataBind
// in Page_Load (with is relatively common), this control will already have been databound even
// though pagePreLoad never fired and the page isn't a postback.
if (!Page.IsPostBack)
{
RequiresDataBinding = true;
}
// If the control was added to the page after page.PreLoad, we'll never get the event and we'll
// never databind the control. So if we're catching up and Load happens but PreLoad never happened,
// call DataBind. This may make the control get databound twice if the user called DataBind on the control
// directly in Page.OnLoad, but better to bind twice than never to bind at all.
else if (IsViewStateEnabled)
{
RequiresDataBinding = true;
}
}
base.OnLoad(e);
}
protected override void CreateChildControls()
{
base.CreateChildControls();
// If we already have items in the ViewState, create the control
// hierarchy using the view state (and not the datasource)
if (AccordionBehavior != null && ViewState[ItemCountViewStateKey] != null)
CreateControlHierarchy(false);
ClearChildViewState();
}
protected override void OnPreRender(EventArgs e)
{
EnsureDataBound();
base.OnPreRender(e);
// Set the overflow to hidden to prevent any growth from
// showing initially before it is hidden by the script if
// we are controlling the height
if (AutoSize != APXAccordionAutoSize.None)
{
Style[HtmlTextWriterStyle.Overflow] = "hidden";
Style[HtmlTextWriterStyle.OverflowX] = "auto";
}
// Apply the standard header/content styles, but allow the
// pane's styles to take precedent
foreach (APXAccordionPane pane in Panes)
{
if (!string.IsNullOrEmpty(HeaderCssClass) && string.IsNullOrEmpty(pane.HeaderCssClass))
pane.HeaderCssClass = HeaderCssClass;
if (!string.IsNullOrEmpty(ContentCssClass) && string.IsNullOrEmpty(pane.ContentCssClass))
pane.ContentCssClass = ContentCssClass;
}
// Get the index of the selected pane, or use the first pane if we don't
// have a valid index and require one. (Note: We don't reset the SelectedIndex
// property because it may refer to a pane that will be added dynamically on the
// client. If we need to start with a pane visible, then we'll open the first
// pane because that's the default value used on the client as the SelectedIndex
// in this scenario.)
int index = AccordionBehavior.SelectedIndex;
index = ((index < 0 || index >= Panes.Count) && AccordionBehavior.RequireOpenedPane) ? 0 : index;
// Make sure the selected pane is displayed
if (index >= 0 && index < Panes.Count)
{
APXAccordionContentPanel content = Panes[index].ContentContainer;
if (content != null)
content.Collapsed = false;
// Set the CSS class for the open panes header
if (!string.IsNullOrEmpty(HeaderSelectedCssClass))
Panes[index].HeaderCssClass = HeaderSelectedCssClass;
}
}
public override Control FindControl(string id)
{
Control ctrl = base.FindControl(id);
if (ctrl == null)
foreach (APXAccordionPane pane in Panes)
{
ctrl = pane.FindControl(id);
if (ctrl != null)
break;
}
return ctrl;
}
#endregion
}
}