using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Data.Spatial; using System.Diagnostics; using System.Globalization; using System.Web.Compilation; using System.Web.Hosting; using System.Web.UI.WebControls; namespace System.Web.DynamicData { /// /// Default implementation of IFieldTemplateFactory. It uses user controls for the field templates. /// public class FieldTemplateFactory : IFieldTemplateFactory { private const string IntegerField = "Integer"; private const string ForeignKeyField = "ForeignKey"; private const string ChildrenField = "Children"; private const string ManyToManyField = "ManyToMany"; private const string EnumerationField = "Enumeration"; private const string EditModePathModifier = "_Edit"; private const string InsertModePathModifier = "_Insert"; private Dictionary _typesToTemplateNames; private Dictionary _typesFallBacks; private TemplateFactory _templateFactory; /// /// public FieldTemplateFactory() { InitTypesToTemplateNamesTable(); BuildTypeFallbackTable(); _templateFactory = new TemplateFactory("FieldTemplates"); } // For unit test purpose internal FieldTemplateFactory(VirtualPathProvider vpp) : this() { _templateFactory.VirtualPathProvider = vpp; } /// /// Sets the folder containing the user controls. By default, this is ~/DynamicData/FieldTemplates/ /// public string TemplateFolderVirtualPath { get { return _templateFactory.TemplateFolderVirtualPath; } set { _templateFactory.TemplateFolderVirtualPath = value; } } /// /// The MetaModel that the factory is associated with /// public MetaModel Model { get { return _templateFactory.Model; } private set { _templateFactory.Model = value; } } private void InitTypesToTemplateNamesTable() { _typesToTemplateNames = new Dictionary(); _typesToTemplateNames[typeof(int)] = FieldTemplateFactory.IntegerField; _typesToTemplateNames[typeof(string)] = DataType.Text.ToString(); } private void BuildTypeFallbackTable() { _typesFallBacks = new Dictionary(); _typesFallBacks[typeof(float)] = typeof(decimal); _typesFallBacks[typeof(double)] = typeof(decimal); _typesFallBacks[typeof(Int16)] = typeof(int); _typesFallBacks[typeof(byte)] = typeof(int); _typesFallBacks[typeof(long)] = typeof(int); // Fall back to strings for most types _typesFallBacks[typeof(char)] = typeof(string); _typesFallBacks[typeof(int)] = typeof(string); _typesFallBacks[typeof(decimal)] = typeof(string); _typesFallBacks[typeof(Guid)] = typeof(string); _typesFallBacks[typeof(DateTime)] = typeof(string); _typesFallBacks[typeof(DateTimeOffset)] = typeof(string); _typesFallBacks[typeof(TimeSpan)] = typeof(string); _typesFallBacks[typeof(DbGeography)] = typeof(string); _typesFallBacks[typeof(DbGeometry)] = typeof(string); // } private Type GetFallBackType(Type t) { // Check if there is a fallback type Type fallbackType; if (_typesFallBacks.TryGetValue(t, out fallbackType)) return fallbackType; return null; } // Internal for unit test purpose internal string GetFieldTemplateVirtualPathWithCaching(MetaColumn column, DataBoundControlMode mode, string uiHint) { // Compute a cache key based on all the input paramaters long cacheKey = Misc.CombineHashCodes(uiHint, column, mode); Func templatePathFactoryFunction = () => GetFieldTemplateVirtualPath(column, mode, uiHint); return _templateFactory.GetTemplatePath(cacheKey, templatePathFactoryFunction); } /// /// Returns the virtual path of the field template user control to be used, based on various pieces of data /// /// The MetaColumn for which the field template is needed /// The mode (Readonly, Edit, Insert) for which the field template is needed /// The UIHint (if any) that should affect the field template lookup /// public virtual string GetFieldTemplateVirtualPath(MetaColumn column, DataBoundControlMode mode, string uiHint) { mode = PreprocessMode(column, mode); bool hasDataTypeAttribute = column != null && column.DataTypeAttribute != null; // Set the UIHint in some special cases, but don't do it if we already have one or // if we have a DataTypeAttribute if (String.IsNullOrEmpty(uiHint) && !hasDataTypeAttribute) { // Check if it's an association // Or if it is an enum if (column is MetaForeignKeyColumn) { uiHint = FieldTemplateFactory.ForeignKeyField; } else if (column is MetaChildrenColumn) { var childrenColumn = (MetaChildrenColumn)column; if (childrenColumn.IsManyToMany) { uiHint = FieldTemplateFactory.ManyToManyField; } else { uiHint = FieldTemplateFactory.ChildrenField; } } else if (column.ColumnType.IsEnum) { uiHint = FieldTemplateFactory.EnumerationField; } } return GetVirtualPathWithModeFallback(uiHint, column, mode); } /// /// Gets a chance to change the mode. e.g. an Edit mode request can be turned into ReadOnly mode /// if the column is a primary key /// public virtual DataBoundControlMode PreprocessMode(MetaColumn column, DataBoundControlMode mode) { if (column == null) { throw new ArgumentNullException("column"); } // Primary keys can't be edited, so put them in readonly mode. Note that this // does not apply to Insert mode, which is fine if (column.IsPrimaryKey && mode == DataBoundControlMode.Edit) { mode = DataBoundControlMode.ReadOnly; } // Generated columns should never be editable/insertable if (column.IsGenerated) { mode = DataBoundControlMode.ReadOnly; } // ReadOnly columns cannot be edited nor inserted, and are always in Display mode if (column.IsReadOnly) { if (mode == DataBoundControlMode.Insert && column.AllowInitialValue) { // but don't change the mode if we're in insert and an initial value is allowed } else { mode = DataBoundControlMode.ReadOnly; } } // If initial value is not allowed set mode to ReadOnly if (mode == DataBoundControlMode.Insert && !column.AllowInitialValue) { mode = DataBoundControlMode.ReadOnly; } if (column is MetaForeignKeyColumn) { // If the foreign key is part of the primary key (e.g. Order and Product in Order_Details table), // change the mode to ReadOnly so that they can't be edited. if (mode == DataBoundControlMode.Edit && ((MetaForeignKeyColumn)column).IsPrimaryKeyInThisTable) { mode = DataBoundControlMode.ReadOnly; } } return mode; } private string GetVirtualPathWithModeFallback(string templateName, MetaColumn column, DataBoundControlMode mode) { // Try not only the requested mode, but others if needed. Basically: // - an edit template can default to an item template // - an insert template can default to an edit template, then to an item template for (var currentMode = mode; currentMode >= 0; currentMode--) { string virtualPath = GetVirtualPathForMode(templateName, column, currentMode); if (virtualPath != null) return virtualPath; } // We couldn't locate any field template at all, so give up return null; } private string GetVirtualPathForMode(string templateName, MetaColumn column, DataBoundControlMode mode) { // If we got a template name, try it if (!String.IsNullOrEmpty(templateName)) { string virtualPath = GetVirtualPathIfExists(templateName, column, mode); if (virtualPath != null) return virtualPath; } // Otherwise, use the column's type return GetVirtualPathForTypeWithFallback(column.ColumnType, column, mode); } private string GetVirtualPathForTypeWithFallback(Type fieldType, MetaColumn column, DataBoundControlMode mode) { string templateName; string virtualPath; // If we have a data type attribute if (column.DataTypeAttribute != null) { templateName = column.DataTypeAttribute.GetDataTypeName(); // Try to get the path from it virtualPath = GetVirtualPathIfExists(templateName, column, mode); if (virtualPath != null) return virtualPath; } // Try the actual fully qualified type name (i.e. with the namespace) virtualPath = GetVirtualPathIfExists(fieldType.FullName, column, mode); if (virtualPath != null) return virtualPath; // Try the simple type name virtualPath = GetVirtualPathIfExists(fieldType.Name, column, mode); if (virtualPath != null) return virtualPath; // If our type name table has an entry for it, try it if (_typesToTemplateNames.TryGetValue(fieldType, out templateName)) { virtualPath = GetVirtualPathIfExists(templateName, column, mode); if (virtualPath != null) return virtualPath; } // Check if there is a fallback type Type fallbackType = GetFallBackType(fieldType); // If not, we've run out of options if (fallbackType == null) return null; // If so, try it return GetVirtualPathForTypeWithFallback(fallbackType, column, mode); } private string GetVirtualPathIfExists(string templateName, MetaColumn column, DataBoundControlMode mode) { // Build the path string virtualPath = BuildVirtualPath(templateName, column, mode); // Check if it exists if (_templateFactory.FileExists(virtualPath)) return virtualPath; // If not, return null return null; } /// /// Build the virtual path to the field template user control based on the template name and mode. /// By default, it returns names that look like TemplateName_ModeName.ascx, in the folder specified /// by TemplateFolderVirtualPath. /// /// /// /// /// public virtual string BuildVirtualPath(string templateName, MetaColumn column, DataBoundControlMode mode) { if (String.IsNullOrEmpty(templateName)) { throw new ArgumentNullException("templateName"); } string modePathModifier = null; switch (mode) { case DataBoundControlMode.ReadOnly: modePathModifier = String.Empty; break; case DataBoundControlMode.Edit: modePathModifier = FieldTemplateFactory.EditModePathModifier; break; case DataBoundControlMode.Insert: modePathModifier = FieldTemplateFactory.InsertModePathModifier; break; default: Debug.Assert(false); break; } return String.Format(CultureInfo.InvariantCulture, TemplateFolderVirtualPath + "{0}{1}.ascx", templateName, modePathModifier); } #region IFieldTemplateFactory Members public virtual void Initialize(MetaModel model) { Model = model; } /// /// See IFieldTemplateFactory for details. /// /// public virtual IFieldTemplate CreateFieldTemplate(MetaColumn column, DataBoundControlMode mode, string uiHint) { string fieldTemplatePath = GetFieldTemplateVirtualPathWithCaching(column, mode, uiHint); if (fieldTemplatePath == null) return null; return (IFieldTemplate)BuildManager.CreateInstanceFromVirtualPath( fieldTemplatePath, typeof(IFieldTemplate)); } #endregion } }