using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Web.DynamicData.Util; using System.Web.Resources; using System.Web.UI; using System.Web.UI.WebControls; namespace System.Web.DynamicData { /// /// The base class for all field template user controls /// public class FieldTemplateUserControl : UserControl, IBindableControl, IFieldTemplate { private static RequiredAttribute s_defaultRequiredAttribute = new RequiredAttribute(); private Dictionary _ignoredModelValidationAttributes; private object _fieldValue; private DefaultValueMapping _defaultValueMapping; private bool _pageDataItemSet; private object _pageDataItem; public FieldTemplateUserControl() { } internal FieldTemplateUserControl(DefaultValueMapping defaultValueMapping) { _defaultValueMapping = defaultValueMapping; } /// /// The host that provides context to this field template /// [Browsable(false)] public IFieldTemplateHost Host { get; private set; } /// /// The formatting options that need to be applied to this field template /// [Browsable(false)] public IFieldFormattingOptions FormattingOptions { get; private set; } /// /// The MetaColumn that this field template is working with /// [Browsable(false)] public MetaColumn Column { get { return Host.Column; } } /// /// The ContainerType in which this /// [Browsable(false)] public virtual ContainerType ContainerType { get { return Misc.FindContainerType(this); } } /// /// The MetaTable that this field's column belongs to /// [Browsable(false)] public MetaTable Table { get { return Column.Table; } } /// /// Casts the MetaColumn to a MetaForeignKeyColumn. Throws if it is not an FK column. /// [Browsable(false)] public MetaForeignKeyColumn ForeignKeyColumn { get { var foreignKeyColumn = Column as MetaForeignKeyColumn; if (foreignKeyColumn == null) { throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, DynamicDataResources.FieldTemplateUserControl_ColumnIsNotFK, Column.Name)); } return foreignKeyColumn; } } /// /// Casts the MetaColumn to a MetaChildrenColumn. Throws if it is not an Children column. /// [Browsable(false)] public MetaChildrenColumn ChildrenColumn { get { var childrenColumn = Column as MetaChildrenColumn; if (childrenColumn == null) { throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, DynamicDataResources.FieldTemplateUserControl_ColumnIsNotChildren, Column.Name)); } return childrenColumn; } } /// /// The mode (readonly, edit, insert) that the field template should use /// [Browsable(false)] public DataBoundControlMode Mode { get { return Host.Mode; } } /// /// The collection of metadata attributes that apply to this column /// [Browsable(false)] public System.ComponentModel.AttributeCollection MetadataAttributes { get { return Column.Attributes; } } /// /// Returns the data control that handles the field inside the field template /// [Browsable(false)] public virtual Control DataControl { get { return null; } } /// /// The current data object. Equivalent to Page.GetDataItem() /// [Browsable(false)] public virtual object Row { get { // The DataItem is normally null in insert mode, we're going to surface the DictionaryCustomTypeDescriptor if there is a // a default value was specified for this column. if (Mode == DataBoundControlMode.Insert && DefaultValueMapping != null && DefaultValueMapping.Contains(Column)) { return DefaultValueMapping.Instance; } // Used for unit testing. We can't use null since thats a valid value. if (_pageDataItemSet) { return _pageDataItem; } return Page.GetDataItem(); } internal set { // Only set in unit tests. _pageDataItem = value; _pageDataItemSet = true; } } /// /// The value of the Column in the current Row /// [Browsable(false)] public virtual object FieldValue { get { // If a field value was explicitly set, use it instead of the usual logic. if (_fieldValue != null) return _fieldValue; return GetColumnValue(Column); } set { _fieldValue = value; } } /// /// Get the value of a specific column in the current row /// /// /// protected virtual object GetColumnValue(MetaColumn column) { object row = Row; if (row != null) { return DataBinder.GetPropertyValue(row, column.Name); } // Fallback on old behavior if (Mode == DataBoundControlMode.Insert) { return column.DefaultValue; } return null; } /// /// Return the field value as a formatted string /// [Browsable(false)] public virtual string FieldValueString { get { // Get the string and preprocess it return FormatFieldValue(FieldValue); } } /// /// Similar to FieldValueString, but the string is to be used when the field is in edit mode /// [Browsable(false)] public virtual string FieldValueEditString { get { return FormattingOptions.FormatEditValue(FieldValue); } } /// /// Only applies to FK columns. Returns a URL that links to the page that displays the details /// of the foreign key entity. e.g. In the Product table's Category column, this produces a link /// that goes to the details of the category that the product is in /// protected string ForeignKeyPath { get { return ForeignKeyColumn.GetForeignKeyPath(PageAction.Details, Row); } } internal DefaultValueMapping DefaultValueMapping { get { if (_defaultValueMapping == null) { // Ensure this only gets accessed in insert mode Debug.Assert(Mode == DataBoundControlMode.Insert); _defaultValueMapping = MetaTableHelper.GetDefaultValueMapping(this, Context.ToWrapper()); } return _defaultValueMapping; } } /// /// Same as ForeignKeyPath, except that it allows the path part of the URL to be overriden. This is /// used when using pages that don't live under DynamicData/CustomPages. /// /// The path override /// protected string BuildForeignKeyPath(string path) { // If a path was passed in, resolved it relative to the containing page if (!String.IsNullOrEmpty(path)) { path = ResolveParentRelativePath(path); } return ForeignKeyColumn.GetForeignKeyPath(PageAction.Details, Row, path); } /// /// Only applies to Children columns. Returns a URL that links to the page that displays the list /// of children entities. e.g. In the Category table's Products column, this produces a link /// that goes to the list of Products that are in this Category. /// protected string ChildrenPath { get { return ChildrenColumn.GetChildrenPath(PageAction.List, Row); } } /// /// Same as ChildrenPath, except that it allows the path part of the URL to be overriden. This is /// used when using pages that don't live under DynamicData/CustomPages. /// /// The path override /// protected string BuildChildrenPath(string path) { // If a path was passed in, resolved it relative to the containing page if (!String.IsNullOrEmpty(path)) { path = ResolveParentRelativePath(path); } return ChildrenColumn.GetChildrenPath(PageAction.List, Row, path); } // Resolve a relative path based on the containing page private string ResolveParentRelativePath(string path) { if (path == null || TemplateControl == null) return path; Control parentControl = TemplateControl.Parent; if (parentControl == null) return path; return parentControl.ResolveUrl(path); } /// /// Return the field template for another column /// protected FieldTemplateUserControl FindOtherFieldTemplate(string columnName) { return Parent.FindFieldTemplate(columnName) as FieldTemplateUserControl; } /// /// Only applies to FK columns. Populate the list control with all the values from the parent table /// /// The control to be populated protected void PopulateListControl(ListControl listControl) { Type enumType; if (Column is MetaForeignKeyColumn) { Misc.FillListItemCollection(ForeignKeyColumn.ParentTable, listControl.Items); } else if (Column.IsEnumType(out enumType)) { Debug.Assert(enumType != null); FillEnumListControl(listControl, enumType); } } private void FillEnumListControl(ListControl list, Type enumType) { foreach (DictionaryEntry entry in Misc.GetEnumNamesAndValues(enumType)) { list.Items.Add(new ListItem((string)entry.Key, (string)entry.Value)); } } /// /// Gets a string representation of the column's value so that it can be matched with /// values populated in a dropdown. This currently works for FK and Enum columns only. /// The method returns null for other column types. /// /// protected string GetSelectedValueString() { Type enumType; if (Column is MetaForeignKeyColumn) { return ForeignKeyColumn.GetForeignKeyString(Row); } else if(Column.IsEnumType(out enumType)) { return Misc.GetUnderlyingTypeValueString(enumType, FieldValue); } return null; } /// /// Only applies to FK columns. This is used when saving the value of a foreign key, typically selected /// from a drop down. /// /// The dictionary that contains all the new values /// The value to be saved. Typically, this comes from DropDownList.SelectedValue protected virtual void ExtractForeignKey(IDictionary dictionary, string selectedValue) { ForeignKeyColumn.ExtractForeignKey(dictionary, selectedValue); } /// /// Apply potential HTML encoding and formatting to a string that needs to be displayed /// /// The value that should be formatted /// the formatted value public virtual string FormatFieldValue(object fieldValue) { return FormattingOptions.FormatValue(fieldValue); } /// /// Return either the input value or null based on ConvertEmptyStringToNull and NullDisplayText /// /// The input value /// The converted value protected virtual object ConvertEditedValue(string value) { return FormattingOptions.ConvertEditedValue(value); } /// /// Set up a validator for dynamic data use. It sets the ValidationGroup on all validators, /// and also performs additional logic for some specific validator types. e.g. for a RangeValidator /// it sets the range values if they exist on the model. /// /// The validator to be set up [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", Justification = "We really want Set Up as two words")] protected virtual void SetUpValidator(BaseValidator validator) { SetUpValidator(validator, Column); } /// /// Set up a validator for dynamic data use. It sets the ValidationGroup on all validators, /// and also performs additional logic for some specific validator types. e.g. for a RangeValidator /// it sets the range values if they exist on the model. /// /// The validator to be set up /// The column for which the validator is getting set [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", Justification = "We really want Set Up as two words")] protected virtual void SetUpValidator(BaseValidator validator, MetaColumn column) { // Set the validation group to match the dynamic control validator.ValidationGroup = Host.ValidationGroup; if (validator is DynamicValidator) { SetUpDynamicValidator((DynamicValidator)validator, column); } else if (validator is RequiredFieldValidator) { SetUpRequiredFieldValidator((RequiredFieldValidator)validator, column); } else if (validator is CompareValidator) { SetUpCompareValidator((CompareValidator)validator, column); } else if (validator is RangeValidator) { SetUpRangeValidator((RangeValidator)validator, column); } else if (validator is RegularExpressionValidator) { SetUpRegexValidator((RegularExpressionValidator)validator, column); } validator.ToolTip = validator.ErrorMessage; validator.Text = "*"; } private void SetUpDynamicValidator(DynamicValidator validator, MetaColumn column) { validator.Column = column; // Tell the DynamicValidator which validation attributes it should ignore (because // they're already handled by server side ASP.NET validator controls) validator.SetIgnoredModelValidationAttributes(_ignoredModelValidationAttributes); } private void SetUpRequiredFieldValidator(RequiredFieldValidator validator, MetaColumn column) { var requiredAttribute = column.Metadata.RequiredAttribute; if (requiredAttribute!= null && requiredAttribute.AllowEmptyStrings) { // Dev10 Bug 749744 // If somone explicitly set AllowEmptyStrings = true then we assume that they want to // allow empty strings to go into a database even if the column is marked as required. // Since ASP.NET validators always get an empty string, this essential turns of // required field validation. IgnoreModelValidationAttribute(typeof(RequiredAttribute)); } else if (column.IsRequired) { validator.Enabled = true; // Make sure the attribute doesn't get validated a second time by the DynamicValidator IgnoreModelValidationAttribute(typeof(RequiredAttribute)); if (String.IsNullOrEmpty(validator.ErrorMessage)) { string columnErrorMessage = column.RequiredErrorMessage; if (String.IsNullOrEmpty(columnErrorMessage)) { // generate default error message validator.ErrorMessage = HttpUtility.HtmlEncode(s_defaultRequiredAttribute.FormatErrorMessage(column.DisplayName)); } else { validator.ErrorMessage = HttpUtility.HtmlEncode(columnErrorMessage); } } } } private void SetUpCompareValidator(CompareValidator validator, MetaColumn column) { validator.Operator = ValidationCompareOperator.DataTypeCheck; ValidationDataType? dataType = null; string errorMessage = null; if (column.ColumnType == typeof(DateTime)) { dataType = ValidationDataType.Date; errorMessage = String.Format(CultureInfo.CurrentCulture, DynamicDataResources.FieldTemplateUserControl_CompareValidationError_Date, column.DisplayName); } else if (column.IsInteger && column.ColumnType != typeof(long)) { // long is unsupported because it's larger than int dataType = ValidationDataType.Integer; errorMessage = String.Format(CultureInfo.CurrentCulture, DynamicDataResources.FieldTemplateUserControl_CompareValidationError_Integer, column.DisplayName); } else if (column.ColumnType == typeof(decimal)) { // dataType = ValidationDataType.Double; errorMessage = String.Format(CultureInfo.CurrentCulture, DynamicDataResources.FieldTemplateUserControl_CompareValidationError_Decimal, column.DisplayName); } else if (column.IsFloatingPoint) { dataType = ValidationDataType.Double; errorMessage = String.Format(CultureInfo.CurrentCulture, DynamicDataResources.FieldTemplateUserControl_CompareValidationError_Decimal, column.DisplayName); } if (dataType != null) { Debug.Assert(errorMessage != null); validator.Enabled = true; validator.Type = dataType.Value; if (String.IsNullOrEmpty(validator.ErrorMessage)) { validator.ErrorMessage = HttpUtility.HtmlEncode(errorMessage); } } else { // If we don't recognize the type, turn off the validator validator.Enabled = false; } } private void SetUpRangeValidator(RangeValidator validator, MetaColumn column) { // Nothing to do if no range was specified var rangeAttribute = column.Attributes.OfType().FirstOrDefault(); if (rangeAttribute == null) return; // Make sure the attribute doesn't get validated a second time by the DynamicValidator IgnoreModelValidationAttribute(rangeAttribute.GetType()); validator.Enabled = true; Func converter; switch (validator.Type) { case ValidationDataType.Integer: converter = val => Convert.ToInt32(val, CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture); break; case ValidationDataType.Double: converter = val => Convert.ToDouble(val, CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture); break; case ValidationDataType.String: default: converter = val => val.ToString(); break; } validator.MinimumValue = converter(rangeAttribute.Minimum); validator.MaximumValue = converter(rangeAttribute.Maximum); if (String.IsNullOrEmpty(validator.ErrorMessage)) { validator.ErrorMessage = HttpUtility.HtmlEncode( StringLocalizerUtil.GetLocalizedString(rangeAttribute, column.DisplayName)); } } private void SetUpRegexValidator(RegularExpressionValidator validator, MetaColumn column) { // Nothing to do if no regex was specified var regexAttribute = column.Attributes.OfType().FirstOrDefault(); if (regexAttribute == null) return; // Make sure the attribute doesn't get validated a second time by the DynamicValidator IgnoreModelValidationAttribute(regexAttribute.GetType()); validator.Enabled = true; validator.ValidationExpression = regexAttribute.Pattern; if (String.IsNullOrEmpty(validator.ErrorMessage)) { validator.ErrorMessage = HttpUtility.HtmlEncode( StringLocalizerUtil.GetLocalizedString(regexAttribute, column.DisplayName)); } } /// /// This method instructs the DynamicValidator to ignore a specific type of model /// validation attributes. This is called when that attribute type is already being /// fully handled by an ASP.NET validator controls. Without this call, the validation /// could happen twice, resulting in a duplicated error message /// [SuppressMessage("Microsoft.Usage", "CA2301:EmbeddableTypesInContainersRule", MessageId = "_ignoredModelValidationAttributes", Justification = "The types that go into this dictionary are specifically ValidationAttribute derived types.")] protected void IgnoreModelValidationAttribute(Type attributeType) { // Create the dictionary on demand if (_ignoredModelValidationAttributes == null) { _ignoredModelValidationAttributes = new Dictionary(); } // Add the attribute type to the list _ignoredModelValidationAttributes[attributeType] = true; } /// /// Implementation of IBindableControl.ExtractValues /// /// The dictionary that contains all the new values protected virtual void ExtractValues(IOrderedDictionary dictionary) { // To nothing in the base class. Derived field templates decide what they want to save } #region IBindableControl Members void IBindableControl.ExtractValues(IOrderedDictionary dictionary) { ExtractValues(dictionary); } #endregion #region IFieldTemplate Members void IFieldTemplate.SetHost(IFieldTemplateHost host) { Host = host; FormattingOptions = Host.FormattingOptions; } #endregion } }