I am using a GridView to display data where one of the data columns has type DateTimeOffset. In order to display dates & times in the user's timezone, I save the user's timezone preference to his or her profile (property value key "TimezoneOffset"), and need to access it when formatting dates & times.
If I were to use templatefield, then I would need to write:
<abbr class="datetimeoffset">
<%#
((DateTimeOffset)Eval("CreatedDate"))
.ToOffset(new TimeSpan(-((Int32)Profile.GetPropertyValue("TimezoneOffset"))
.ToRepresentativeInRange(-12, 24), 0, 0)).ToString("f") %>
</abbr>
which is too complicated and not reusable.
I tried adding a TimeSpan property to the code-behind (to at least move that out of the data binding expression), but apparently properties of the view's code-behind are inaccessible within <%# ... %>.
Therefore, I think that I need to write a custom DataControlField to format dates & times in the user's timezone.
I have started with:
public class DateTimeOffsetField : DataControlField
{
private TimeSpan userOffsetTimeSpan;
protected override DataControlField CreateField()
{
return new DateTimeOffsetField();
}
protected override void CopyProperties(DataControlField newField)
{
base.CopyProperties(newField);
((DateTimeOffsetField)newField).userOffsetTimeSpan = userOffsetTimeSpan;
}
public override bool Initialize(bool sortingEnabled, System.Web.UI.Control control)
{
bool ret = base.Initialize(sortingEnabled, control);
int timezoneOffset = ((Int32)HttpContext.Current.Profile.GetPropertyValue("TimezoneOffset")).ToRepresentativeInRange(-12, 24);
userOffsetTimeSpan = new TimeSpan(-timezoneOffset, 0, 0);
return ret;
}
}
But now I am stuck. How do I output the HTML <abbr class="datetimeoffset"><%# ((DateTimeOffset)Eval("CreatedDate")).ToOffset(userOffsetTimeSpan).ToString("f") %></abbr> for each cell?
EDIT: I have been reading an article titled Cutting Edge: Custom Data Control Fields. So far I have added:
public override void InitializeCell(DataControlFieldCell cell, DataControlCellType cellType, DataControlRowState rowState, int rowIndex)
{
base.InitializeCell(cell, cellType, rowState, rowIndex);
if (cellType == DataControlCellType.DataCell)
{
InitializeDataCell(cell, rowState, rowIndex);
}
}
protected virtual void InitializeDataCell(DataControlFieldCell cell, DataControlRowState rowState, int rowIndex)
{
System.Web.UI.Control control = cell;
if (control != null && Visible)
{
control.DataBinding += new EventHandler(OnBindingField);
}
}
protected virtual void OnBindingField(object sender, EventArgs e)
{
var target = (System.Web.UI.Control)sender;
if (target is TableCell)
{
TableCell tc = (TableCell)target;
}
}
but whereas the article sets the Text property of the TableCell instance, I would like to render a partial view into the table cell. Is that possible?
I figured it out. Here is what I ended up with:
// DateTimeOffsetField.cs
public class DateTimeOffsetField : BoundField
{
private TimeSpan userOffsetTimeSpan;
protected override DataControlField CreateField()
{
return new DateTimeOffsetField();
}
protected override void CopyProperties(DataControlField newField)
{
base.CopyProperties(newField);
((DateTimeOffsetField)newField).userOffsetTimeSpan = userOffsetTimeSpan;
}
public override bool Initialize(bool sortingEnabled, System.Web.UI.Control control)
{
bool ret = base.Initialize(sortingEnabled, control);
int timezoneOffset = ((Int32)HttpContext.Current.Profile.GetPropertyValue("TimezoneOffset")).ToRepresentativeInRange(-12, 24);
userOffsetTimeSpan = new TimeSpan(-timezoneOffset, 0, 0);
return ret;
}
protected override void OnDataBindField(object sender, EventArgs e)
{
base.OnDataBindField(sender, e);
var target = (Control)sender;
if (target is TableCell)
{
var tc = (TableCell)target;
var dataItem = DataBinder.GetDataItem(target.NamingContainer);
var dateTimeOffset = (DateTimeOffset)DataBinder.GetPropertyValue(dataItem, DataField);
tc.Controls.Add(new TimeagoDateTimeOffset { DateTimeOffset = dateTimeOffset.ToOffset(userOffsetTimeSpan) });
}
}
}
TimeagoDateTimeOffset.cs:
[DefaultProperty("DateTimeOffset")]
[ToolboxData("<{0}:TimeagoDateTimeOffset runat=server></{0}:TimeagoDateTimeOffset>")]
public class TimeagoDateTimeOffset : WebControl
{
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public DateTimeOffset DateTimeOffset
{
get { return (DateTimeOffset)ViewState["DateTimeOffset"]; }
set { ViewState["DateTimeOffset"] = value; }
}
protected override void RenderContents(HtmlTextWriter writer)
{
writer.BeginRender();
writer.AddAttribute(HtmlTextWriterAttribute.Class, "timeago", false);
writer.AddAttribute(HtmlTextWriterAttribute.Title, DateTimeOffset.ToString("o"));
writer.RenderBeginTag("abbr");
writer.Write(DateTimeOffset.ToString("d"));
writer.RenderEndTag();
writer.EndRender();
}
}
Related
I am trying to write a custom viewgroup which I currently duplicate a lot across my layout files. It is a really simple one, two textboxes placed within a linearlayout with a vertical orientation and some padding. It is worth mentioning that I use Xamarin to develop this application, but I don't think the issue is Xamarin specific. My implementation is as follows:
[Register("qube.AppListItem")]
class AppListItem : LinearLayout
{
private string m_Label;
private string m_Value;
private int m_LabelSize;
private int m_ValueSize;
private int m_HorizontalPadding;
private int m_VerticalPadding;
private ValueType m_ValueType;
private TextView m_LabelView;
private TextView m_ValueView;
public AppListItem(Context context) : this(context, null)
{
}
public AppListItem(Context context, IAttributeSet attrs) : this(context, attrs, 0)
{
}
public AppListItem(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr)
{
Initialize(context, attrs);
}
private void Initialize(Context context, IAttributeSet attrs)
{
var a = context.ObtainStyledAttributes(attrs, Resource.Styleable.AppListItem, 0, 0);
m_Label = a.GetString(Resource.Styleable.AppListItem_label);
m_Value = a.GetString(Resource.Styleable.AppListItem_value);
m_LabelSize = a.GetDimensionPixelOffset(Resource.Styleable.AppListItem_labelSize, Resource.Dimension.font_small);
m_ValueSize = a.GetDimensionPixelOffset(Resource.Styleable.AppListItem_valueSize, Resource.Dimension.font_medium);
m_HorizontalPadding = a.GetDimensionPixelOffset(Resource.Styleable.AppListItem_horizontalPadding, Resource.Dimension.row_padding);
m_VerticalPadding = a.GetDimensionPixelOffset(Resource.Styleable.AppListItem_verticalPadding, Resource.Dimension.info_list_padding);
m_ValueType = (ValueType)a.GetInt(Resource.Styleable.AppListItem_valueType, (int)ValueType.STRING);
a.Recycle();
Orientation = Orientation.Vertical;
Clickable = true;
SetPadding(m_HorizontalPadding, m_VerticalPadding, m_HorizontalPadding, m_VerticalPadding);
if (Build.VERSION.SdkInt >= BuildVersionCodes.Honeycomb)
{
// If we're running on Honeycomb or newer, then we can use the Theme's
// selectableItemBackground to ensure that the View has a pressed state
TypedValue outValue = new TypedValue();
Context.Theme.ResolveAttribute(Android.Resource.Attribute.SelectableItemBackground, outValue, true);
SetBackgroundResource(outValue.ResourceId);
}
BuildView();
}
private void BuildView()
{
m_LabelView = new TextView(Context);
m_LabelView.LayoutParameters = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.WrapContent);
m_LabelView.TextSize = m_LabelSize;
m_LabelView.Text = m_Label;
AddView(m_LabelView);
if (m_ValueType == ValueType.EDITTEXT)
m_ValueView = new EditText(Context);
else
m_ValueView = new TextView(Context);
m_ValueView.LayoutParameters = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.WrapContent);
m_ValueView.TextSize = m_ValueSize;
m_ValueView.Text = m_Value;
AddView(m_ValueView);
}
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
base.OnMeasure(widthMeasureSpec, heightMeasureSpec);
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);
}
public string Label
{
get { return m_Label; }
set
{
m_Label = value;
m_LabelView.Text = value;
Invalidate();
RequestLayout();
}
}
public string Value
{
get { return m_ValueView.Text; }
set
{
m_ValueView.Text = value;
m_Value = value;
Invalidate();
RequestLayout();
}
}
public enum ValueType
{
STRING, EDITTEXT
}
}
The expected behavior is, as said, two textboxes stacked on top of each other, however, the actual behavior is a view with height 0 (if android:layout_height="wrap_content"). Setting the layout_height to something like 20dp manually just shows a blank view of that height.
Okay, the mistake I made was a silly one, which was to be expected. When I get the dimensions from the typedarray, the default value I pass in are resource id's instead of the actual values associated with those id's. For example:
m_LabelSize = a.GetDimensionPixelOffset(Resource.Styleable.AppListItem_labelSize, Resource.Dimension.font_small);
should be
m_LabelSize = a.GetDimensionPixelOffset(Resource.Styleable.AppListItem_labelSize, Resources.GetDimensionPixelOffset(Resource.Dimension.font_small));
we have a Problem with converters in MvvmCross in Connection with EditText controls in Android:
In our app, the user inserts user data. We have to do some calculation with this data within the converter, and then write the data in our viewmodel.
This works, as long as the user does not revert his entry.
That means, if he uses the back key, the value is correctly edited, until he reaches the last decimal before "." (for example: 55.99, when he reaches the "55.9").
The ".9" will be removed correctly, but the curosor jumps bevor the remaining "55".
How can we resolve this annoying behaviour?
Viewmodel extract:
private Nullable mdValue1 = null;
public Nullable<decimal> Value1
{
get { return mdValue1; }
set
{
SetProperty(ref mdValue1, value);
}
}
private Nullable<decimal> mdValue2;
public Nullable<decimal> Value2
{
get { return mdValue2; }
set
{
SetProperty(ref mdValue2, value, nameof(Value2));
}
}
Converter (simplified):
public class DecimalToStringValueConverter : MvxValueConverter<Nullable<decimal>, string>
{
protected override string Convert(Nullable<decimal> poValue, Type poTargetType, object poParameter, CultureInfo poCulture)
{
if (!poValue.HasValue)
{
return null;
}
return poValue.Value.ToString();
}
protected override Nullable<decimal> ConvertBack(string value, Type targetType, object parameter, CultureInfo culture)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return decimal.Parse(value);
}
}
Configuration
Android Version: 4.4/5.1/7
Platform: Xamarin
It seems what's happening is when the point is removed and converted to a decimal, the resulting change in the value in the ViewModel is then different to the EditText causing the ViewModel to set the value of the EditText.
Example: User enters 59.9 then backspaces 9. This leaves the value in the EditText as 59. which gets parsed as a decimal to 59 to the ViewModel. As 59. is not equal to 59 the ViewModel will update the value in the EditText to 59 this is what cause the cursor to jump to the start.
One quick way to resolve this is to create a custom binding that makes sure that the cursor is always placed at the end of the EditText upon removing the last decimal place. This can be done using SetSelection which positions the cursor in the SetValueImpl method.
public class DecimalEditTextTargetBinding : MvxConvertingTargetBinding
{
protected EditText EditTextControl => Target as EditText;
private IDisposable _subscription;
public DecimalEditTextTargetBinding(EditText target) : base(target)
{
if (target == null)
MvxBindingTrace.Error($"Error - EditText is null in {nameof(DecimalEditTextTargetBinding)}");
}
public override Type TargetType => typeof(string);
public override MvxBindingMode DefaultMode => MvxBindingMode.TwoWay;
protected override void SetValueImpl(object target, object value)
{
((TextView)target).Text = (string)value;
EditTextControl.SetSelection(EditTextControl.Text?.Length ?? 0);
}
public override void SubscribeToEvents()
{
if (EditTextControl == null)
return;
_subscription = EditTextControl.WeakSubscribe<TextView, AfterTextChangedEventArgs>(
nameof(EditTextControl.AfterTextChanged),
EditTextOnAfterTextChanged);
}
private void EditTextOnAfterTextChanged(object sender, AfterTextChangedEventArgs e)
{
FireValueChanged(EditTextControl.Text);
}
protected override void Dispose(bool isDisposing)
{
if (isDisposing)
{
_subscription?.Dispose();
_subscription = null;
}
base.Dispose(isDisposing);
}
}
And then register the custom binding in your Setup.cs:
protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
{
base.FillTargetFactories(registry);
registry.RegisterCustomBindingFactory<EditText>("DecimalText", inputField => new DecimalEditTextTargetBinding(inputField));
}
XML usage:
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal|numberSigned"
local:MvxBind="DecimalText DecimalToString(Value1)" />
Example of Binding for pre MvvmCross 4.4.0:
public class DecimalEditTextTargetBinding : MvxConvertingTargetBinding
{
private bool _subscribed;
public DecimalEditTextTargetBinding(EditText target) : base(target)
{
if (target == null)
MvxBindingTrace.Error($"Error - EditText is null in {nameof(DecimalEditTextTargetBinding)}");
}
protected EditText EditTextControl => Target as EditText;
public override Type TargetType => typeof(string);
public override MvxBindingMode DefaultMode => MvxBindingMode.TwoWay;
protected override void SetValueImpl(object target, object value)
{
((TextView)target).Text = (string)value;
EditTextControl.SetSelection(EditTextControl.Text?.Length ?? 0);
}
public override void SubscribeToEvents()
{
if (EditTextControl == null)
return;
EditTextControl.AfterTextChanged += EditTextOnAfterTextChanged;
_subscribed = true;
}
private void EditTextOnAfterTextChanged(object sender, AfterTextChangedEventArgs e)
{
FireValueChanged(EditTextControl.Text);
}
protected override void Dispose(bool isDisposing)
{
if (isDisposing && EditTextControl != null && _subscribed)
{
EditTextControl.AfterTextChanged -= EditTextOnAfterTextChanged;
_subscribed = false;
}
base.Dispose(isDisposing);
}
}
I have got following UserControl that work just fine but when I publish the project I am facing this error.
.. is not allowed here because it does not extend class 'System.Web.UI.UserControl'
How to fix it?
ASCX
<%# Control Language="C#" AutoEventWireup="true" CodeBehind="DataPagerGridView.ascx.cs" Inherits="VerInformes.DataPagerGridView" %>
C#
public partial class DataPagerGridView : GridView, IPageableItemContainer
{
private static readonly object EventTotalRowCountAvailable = new object();
public int MaximumRows
{
get { return this.PageSize; }
}
public int StartRowIndex
{
get { return this.PageSize * this.PageIndex; }
}
public event EventHandler<PageEventArgs> TotalRowCountAvailable
{
add { base.Events.AddHandler(DataPagerGridView.EventTotalRowCountAvailable, value); }
remove { base.Events.RemoveHandler(DataPagerGridView.EventTotalRowCountAvailable, value); }
}
public void SetPageProperties(int startRowIndex, int maximumRows, bool databind)
{
int newPageIndex = (startRowIndex / maximumRows);
this.PageSize = maximumRows;
if (this.PageIndex != newPageIndex)
{
bool isCanceled = false;
if (databind)
{
// create the event arguments and raise the event
GridViewPageEventArgs args = new GridViewPageEventArgs(newPageIndex);
this.OnPageIndexChanging(args);
isCanceled = args.Cancel;
newPageIndex = args.NewPageIndex;
}
// if the event wasn't cancelled change the paging values
if (!isCanceled)
{
this.PageIndex = newPageIndex;
if (databind)
this.OnPageIndexChanged(EventArgs.Empty);
}
if (databind)
this.RequiresDataBinding = true;
}
}
protected virtual void OnTotalRowCountAvailable(PageEventArgs e)
{
EventHandler<PageEventArgs> handler = (EventHandler<PageEventArgs>)base.Events[DataPagerGridView.EventTotalRowCountAvailable];
if (handler != null)
{
handler(this, e);
}
}
protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
int rows = base.CreateChildControls(dataSource, dataBinding);
// if the paging feature is enabled, determine the total number of rows in the datasource
if (this.AllowPaging)
{
// if we are databinding, use the number of rows that were created,
// otherwise cast the datasource to an Collection and use that as the count
int totalRowCount = dataBinding ? rows : ((ICollection)dataSource).Count;
// raise the row count available event
IPageableItemContainer pageableItemContainer = this as IPageableItemContainer;
this.OnTotalRowCountAvailable(new PageEventArgs
(pageableItemContainer.StartRowIndex, pageableItemContainer.MaximumRows, totalRowCount));
// make sure the top and bottom pager rows are not visible
if (this.TopPagerRow != null)
this.TopPagerRow.Visible = false;
if (this.BottomPagerRow != null)
this.BottomPagerRow.Visible = false;
}
return rows;
}
protected void Page_Load(object sender, EventArgs e)
{
}
}
I just reorganized the code so it is correct now.
public class MyDataPagerGridView : GridView, IPageableItemContainer
{
private static readonly object EventTotalRowCountAvailable = new object();
public int MaximumRows
{
get { return this.PageSize; }
}
public int StartRowIndex
{
get { return this.PageSize * this.PageIndex; }
}
public event EventHandler<PageEventArgs> TotalRowCountAvailable
{
add { base.Events.AddHandler(MyDataPagerGridView.EventTotalRowCountAvailable, value); }
remove { base.Events.RemoveHandler(MyDataPagerGridView.EventTotalRowCountAvailable, value); }
}
public void SetPageProperties(int startRowIndex, int maximumRows, bool databind)
{
int newPageIndex = (startRowIndex / maximumRows);
this.PageSize = maximumRows;
if (this.PageIndex != newPageIndex)
{
bool isCanceled = false;
if (databind)
{
// create the event arguments and raise the event
GridViewPageEventArgs args = new GridViewPageEventArgs(newPageIndex);
this.OnPageIndexChanging(args);
isCanceled = args.Cancel;
newPageIndex = args.NewPageIndex;
}
// if the event wasn't cancelled change the paging values
if (!isCanceled)
{
this.PageIndex = newPageIndex;
if (databind)
this.OnPageIndexChanged(EventArgs.Empty);
}
if (databind)
this.RequiresDataBinding = true;
}
}
protected virtual void OnTotalRowCountAvailable(PageEventArgs e)
{
EventHandler<PageEventArgs> handler = (EventHandler<PageEventArgs>)base.Events[MyDataPagerGridView.EventTotalRowCountAvailable];
if (handler != null)
{
handler(this, e);
}
}
protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
int rows = base.CreateChildControls(dataSource, dataBinding);
// if the paging feature is enabled, determine the total number of rows in the datasource
if (this.AllowPaging)
{
// if we are databinding, use the number of rows that were created,
// otherwise cast the datasource to an Collection and use that as the count
int totalRowCount = dataBinding ? rows : ((ICollection)dataSource).Count;
// raise the row count available event
IPageableItemContainer pageableItemContainer = this as IPageableItemContainer;
this.OnTotalRowCountAvailable(new PageEventArgs
(pageableItemContainer.StartRowIndex, pageableItemContainer.MaximumRows, totalRowCount));
// make sure the top and bottom pager rows are not visible
if (this.TopPagerRow != null)
this.TopPagerRow.Visible = false;
if (this.BottomPagerRow != null)
this.BottomPagerRow.Visible = false;
}
return rows;
}
}
public partial class DataPagerGridView : UserControl
{
public MyDataPagerGridView DataPagerGrid = new MyDataPagerGridView();
protected void Page_Load(object sender, EventArgs e)
{
}
}
I have a collection of generated custom controls that extend CompositeControl, defined as:
[PersistChildren(true)]
[ToolboxData("<{0}:ContractControl runat=server></{0}:ContractControl>")]
public class ContractControl : CompositeControl
{
private int contractID = 0;
private ContractTileControl tileControl = null;
private ContractDetailControl detailControl = null;
private HtmlGenericControl contractMainDiv = null;
public int ContractID
{
get { return this.contractID; }
set { this.contractID = value; }
}
public ContractTileControl TileControl
{
get { return this.tileControl; }
set { this.tileControl = value; }
}
public ContractDetailControl DetailControl
{
get { return this.detailControl; }
set { this.detailControl = value; }
}
public ContractControl()
{
this.contractMainDiv = new HtmlGenericControl("div");
this.contractMainDiv.ID = "contractMainDiv";
this.contractMainDiv.Attributes.Add("class", "contractMain");
}
#region protected override void OnPreRender(EventArgs e)
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
//CreateChildControls();
}
#endregion
#region protected override void CreateChildControls()
protected override void CreateChildControls()
{
base.CreateChildControls();
if (tileControl != null)
{
this.contractMainDiv.Controls.Add(tileControl);
}
if (detailControl != null)
{
this.contractMainDiv.Controls.Add(detailControl);
}
this.Controls.Add(contractMainDiv);
//base.CreateChildControls();
}
#endregion
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
CreateChildControls();
}
protected override void OnInit(EventArgs e)
{
base.OnLoad(e);
EnsureChildControls();
}
}
Where ContractTileControl and ContractDetailControl are another custom controls derived from CompositeControl.
When I add them to a asp:PlaceHolder control set they render fine, but when I define a repeater like:
<asp:Repeater ID="myRepeater" runat="server" >
<HeaderTemplate>
<table border="0" cellpadding="0" cellspacing="0">
</HeaderTemplate>
<ItemTemplate>
<tr><td><easit:ContractControl ID="contractControl" runat="server" />
</td></tr>
</ItemTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
</asp:Repeater>
And bind them to it:
private void FillContractPlaceHolder()
{
List<ContractControl> controls = new List<ContractControl>();
foreach(KeyValuePair<Customer, List<TFSContract>> pair in contractList)
{
Label customerNameLbl = new Label();
customerNameLbl.ID = "customerNameLbl";
customerNameLbl.CssClass = "customerName";
customerNameLbl.Text = pair.Key.Name;
contractListPlaceHolder.Controls.Add(customerNameLbl);
foreach (TFSContract contract in pair.Value)
{
ContractStatusBarControl status = new ContractStatusBarControl();
status.WidthPercent = GetFillPercent(contract.NumberOfTasks, contract.NumberOfFinishedTasks);
string[] contractNameParts = Regex.Split(contract.Contract.Name, #"[A-Z]{3}-[0-9|A-Z]{2}-[0-9|A-Z]{2}", RegexOptions.IgnoreCase);
ContractDetailControl detail = new ContractDetailControl();
detail.ContractName = contractNameParts.Last();
detail.DateStarted = contract.StartDate;
detail.DateFinished = contract.FinishDate;
detail.StatusBar = status;
ContractTileControl tile = new ContractTileControl();
Match match = Regex.Match(contract.Contract.Name, #"[A-Z]{3}-[0-9|A-Z]{2}-[0-9|A-Z]{2}", RegexOptions.IgnoreCase);
if (match.Value.Length != 0)
{
tile.ContractNumber = match.Value;
}
tile.ContractTasksFinished = contract.NumberOfFinishedTasks;
tile.ContractTasksTotal = contract.NumberOfTasks;
ContractControl contractControl = new ContractControl();
contractControl.ContractID = contract.Contract.Id;
contractControl.TileControl = tile;
contractControl.DetailControl = detail;
//contractListPlaceHolder.Controls.Add(contractControl);
controls.Add(contractControl);
}
}
myRepeater.DataSource = controls;
myRepeater.DataBind();
}
The table gets created, but only the non-composite part contractMainDiv of ContractControl gets rendered, as the Repeater insists that both tileControl and detailControl are null, even though they are properly set to instances of their respective types.
When the Repeater is data-bound, it creates an instance of the ItemTemplate for each item in the data-source, set its DataItem to the item from the data-source, and data-binds the children.
In this case, the item from the data-source is an instance of your ContractControl, and your ItemTemplate has no data-binding, so you'll end up with a blank instance of the ContractControl for each item you've added to the list.
The quick and dirty solution is to add a handler for the ItemDataBound event of your Repeater, and copy the properties to the real control:
protected void myRepeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
switch (e.Item.ItemType)
{
case ListItemType.Item:
case ListItemType.AlternatingItem:
case ListItemType.SelectedItem:
case ListItemType.EditItem:
{
var source = (ContractControl)e.Item.DataItem;
var destination = (ContractControl)e.Item.FindControl("contractControl");
destination.ContractID = source.ContractID;
destination.TileControl = source.TileControl;
destination.DetailControl = source.DetailControl;
break;
}
}
}
A better solution would be to bind your Repeater to a list of TFSContract objects, and moving the code to build the ContractControl into the ItemDataBound event handler.
EDIT
Updated to only process real items, ignoring headers, footers, etc.
I am trying to create a RadioButtonListWithOther class that extends the RadoButtonList but I can't get the "Other" textbox to render on the page. When I step through while debugging I can see the control in the parent control's Controls collectio but it still doesn't render. Any ideas what I am doing wrong here?
public class RadioButtonListWithOther : RadioButtonList
{
private TextBox _otherReason;
public RadioButtonListWithOther()
{
_otherReason = new TextBox();
_otherReason.TextMode = TextBoxMode.MultiLine;
_otherReason.Rows = 6;
_otherReason.Width = Unit.Pixel(300);
_otherReason.Visible = true;
}
protected override void CreateChildControls()
{
this.Controls.Add(_otherReason);
this.EnsureChildControls();
base.CreateChildControls();
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
_otherReason.Enabled = false;
if (OtherSelected())
{
_otherReason.Enabled = true;
}
base.OnSelectedIndexChanged(e);
}
public override string Text
{
get
{
if (OtherSelected())
{
return _otherReason.Text;
}
return base.Text;
}
set
{
base.Text = value;
}
}
public override bool Visible
{
get
{
return base.Visible;
}
set
{
//Push visibility changes down to the children controls
foreach (Control control in this.Controls)
{
control.Visible = value;
}
base.Visible = value;
}
}
private bool OtherSelected()
{
if (this.SelectedItem.Text == "Other")
{
return true;
}
return false;
}
}
Here is my code to add an instance of this control to the WebForm:
protected override void CreateChildControls()
{
var whyMentorOptions = new Dictionary<string, string>();
whyMentorOptions.Add("Option 1", "1");
whyMentorOptions.Add("Option 2", "2");
whyMentorOptions.Add("Option 3", "3");
whyMentorOptions.Add("Other", "Other");
mentorWhy = new RadioButtonListWithOther
{
DataSource = whyMentorOptions
};
this.mentorWhy.DataTextField = "Key";
this.mentorWhy.DataValueField = "Value";
this.mentorWhy.DataBind();
Form.Controls.Add(mentorWhy);
base.CreateChildControls();
}
The RadioButtonList class completely ignores its child controls when rendering (it's only interested in the contents of its Items collection).
You'll have to render the text box yourself:
protected override void Render(HtmlTextWriter writer)
{
base.Render(writer);
_otherReason.RenderControl(writer);
}