using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Linq; using System.Linq.Expressions; using System.Reflection; using InABox.Clients; namespace InABox.Core { public delegate void DataModelUpdateEvent(string section, DataModel model); [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] public sealed class DataModelTableNameAttribute : Attribute { public string TableName { get; set; } public DataModelTableNameAttribute(string tableName) { TableName = tableName; } } public interface IDataModelSource { string SectionName { get; } DataModel DataModel(Selection selection); event DataModelUpdateEvent? OnUpdateDataModel; } public interface IDataModelRelationship { string ChildColumn { get; } string ChildTable { get; } string ParentColumn { get; } string ParentTable { get; } bool IsLookup { get; } DataRelation AsDataRelation(); string ParentColumnAsPropertyName(); string ChildColumnAsPropertyName(); } public interface IDataModelQueryDef : IQueryDef { string TableName { get; } } public class DataModelQueryDef : IDataModelQueryDef { public DataModelQueryDef(Filter filter, Columns columns, SortOrder sortorder, string? alias = null) { Type = typeof(T); Filter = filter; Columns = columns; SortOrder = sortorder; TableName = DataModel.TableName(Type, alias); } public Type Type { get; } public IFilter Filter { get; } public IColumns Columns { get; } public ISortOrder SortOrder { get; } public string TableName { get; } } public delegate void OnBeforeLoad(CancelEventArgs args); public delegate void OnAfterLoad(CancelEventArgs args); public interface IDataModelTable { string Name { get; set; } CoreTable Data { get; } bool IsDefault { get; set; } bool IsRequired { get; set; } /// /// The type of entity stored in this table; may be if this table does not represent an entity. /// Type? Type { get; } IEnumerable ChildTables { get; } IDataModelTable ChildTable(string alias); IDataModelTable ChildTable(string? alias = null); void Load(); } public interface IDataModel { IEnumerable DefaultTables { get; } void AddTable(string alias, CoreTable table, bool isdefault = false); void AddTable(Type type, CoreTable table, bool isdefault = false, string? alias = null); /// /// Adds a table to the datamodel. /// /// The type of the object represented by the table. /// A filter for the table. If set to , loads all objects of . /// The columns to load for this table. If set to , loads all the columns of . /// /// Is this table default loaded? If set to , this table is added to . /// The name of this table. Defaults to if not set or . /// /// if this table should be loaded - in some cases a table's data should loaded manually.
/// Set to if this is the case. /// void AddTable(Filter? filter, Columns? columns, bool isdefault = false, string? alias = null, bool shouldLoad = true); void LinkTable(Type parenttype, string parentcolumn, Type childtype, string childcolumn, string? parentalias = null, string? childalias = null, bool isLookup = false); void LinkTable(Type parenttype, string parentcolumn, string childalias, string childcolumn, string? parentalias = null, bool isLookup = false); void LinkTable(Expression> parent, Expression> child, string? parentalias = null, string? childalias = null, bool isLookup = false); void AddChildTable(Expression> parentcolumn, Expression> childcolumn, Filter? filter = null, Columns? columns = null, bool isdefault = false, string? parentalias = null, string? childalias = null); void AddLookupTable(Expression> sourcecolumn, Expression> lookupcolumn, Filter? filter = null, Columns? columns = null, bool isdefault = false, string? sourcealias = null, string? lookupalias = null); /// /// Remove a table from the data model. /// /// The type of the table to be removed. /// The table name, defaulting to the name of . /// if the table was removed, if it was not removed because it didn't exist. bool RemoveTable(string? alias = null); /// /// Gets the filter for a given table, which is used during The type of the table to get the filter of. /// The name of the table, defaulting to /// The filter. Filter? GetFilter(string? alias = null); /// /// Gets the columns for a given table, which are used during The type of the table to get the columns of. /// The name of the table, defaulting to /// The columns. Columns? GetColumns(string? alias = null); /// /// Sets the filter for a given table, which is used during The type of the table to set the filter of. /// The new filter. /// The name of the table, defaulting to void SetFilter(Filter? filter, string? alias = null); /// /// Sets the columns for a given table, which are used during The type of the table. /// The new columns. /// The name of the table, defaulting to . void SetColumns(Columns? columns, string? alias = null); void SetIsDefault(bool isDefault, string? alias = null); void SetShouldLoad(bool shouldLoad, string? alias = null); CoreTable GetTable(string? alias = null); void SetTableData(Type type, CoreTable tableData, string? alias = null); bool HasTable(Type type, string? alias = null); bool HasTable(string? alias = null); void LoadModel(IEnumerable? requiredTables, Dictionary? requiredQueries = null); void LoadModel(IEnumerable? requiredTables, params IDataModelQueryDef[] requiredQueries); /// /// Load the model, loading all tables that are set to be default. (See ). /// void LoadModel(); TType[] ExtractValues(Expression> column, bool distinct = true, string? alias = null); } public interface IDataModel : IDataModel where T : Entity, IRemotable, IPersistent, new() { } public class TableDoesNotExistException : Exception { public TableDoesNotExistException(string tableName): base($"No table for {tableName}") { } } public class TableDoesNotExistException : Exception { public TableDoesNotExistException(string tableName) : base($"No table for {tableName} of type {typeof(TTable)}") { } } public class AmbiguousTableException : Exception { public AmbiguousTableException() : base($"Multiple tables of type {typeof(TTable)}") { } public AmbiguousTableException(string alias) : base($"Multiple tables of type {typeof(TTable)} for given alias '{alias}'") { } } public abstract class DataModel : IDataModel { private readonly List _relationships = new List(); private readonly Dictionary _tables = new Dictionary(); public DataModel() { AddTable(null, null, true); AddChildTable(x => x.Logo.ID, x => x.ID, null, null, true, null, "CompanyLogo"); AddTable(new Filter(x => x.ID).IsEqualTo(ClientFactory.UserGuid), null, true); } public IEnumerable> ModelTables => _tables; public abstract string Name { get; } public IEnumerable Relations => _relationships.Select(x => x.AsDataRelation()).ToArray(); public IEnumerable Tables => _tables.Select(x => x.Value.Table.ToDataTable(x.Key)); public IEnumerable DefaultTables => _tables.Where(x => x.Value.IsDefault).Select(x => x.Value.Table.ToDataTable(x.Key)); public IEnumerable DefaultTableNames => _tables.Where(x => x.Value.IsDefault).Select(x => x.Key); public IEnumerable TableNames => _tables.Select(x => x.Key); public TType[] ExtractValues(Expression> column, bool distinct = true, string? alias = null) { return GetTable(alias).ExtractValues(column, distinct).ToArray(); } public DataSet AsDataSet() { var result = new DataSet(); foreach(var (key, table) in _tables) { var current = table.Table.Columns.ToDictionary(x => x.ColumnName, x => x); IColumns? additional = null; if(table.Type != null) { additional = Columns.None(table.Type); foreach (var column in CoreUtils.GetColumnNames(table.Type, x => true)) { if (!current.ContainsKey(column)) { additional.Add(column); } } } var dataTable = table.Table.ToDataTable(key, additional); result.Tables.Add(dataTable); } //result.Tables.AddRange(_tables.Select(x => x.Value.Table.ToDataTable(x.Key)).ToArray()); foreach (var relation in _relationships) { var childTable = result.Tables[relation.ChildTable]; var parentTable = result.Tables[relation.ParentTable]; if (childTable is null) { continue; } if (parentTable is null) { result.Tables.Remove(childTable); continue; } var parentColumn = parentTable.Columns[relation.ParentColumn.Replace(".", "_")]; var childColumn = childTable.Columns[relation.ChildColumn.Replace(".", "_")]; if (parentColumn is null || childColumn is null) { result.Tables.Remove(childTable); continue; } if (relation.IsLookup) { result.Relations.Add( string.Format("{0}_{1}", relation.ChildTable, relation.ParentTable), childColumn, parentColumn, false ); } else { result.Relations.Add( string.Format("{0}_{1}", relation.ParentTable, relation.ChildTable), parentColumn, childColumn, false ); } } return result; } public event OnBeforeLoad? OnBeforeLoad; public event OnAfterLoad? OnAfterLoad; protected virtual void BeforeLoad(IEnumerable requiredtables) { } protected virtual void CheckRequiredTables(List requiredtables) { } //protected abstract void Load(IEnumerable requiredtables); protected virtual void AfterLoad(IEnumerable requiredTables) { } protected void Load(Type type, CoreTable data, IEnumerable requiredtables, string? alias = null) { CheckTable(type, alias); var name = TableName(type, alias); var table = _tables[name].Table; if(!ReferenceEquals(table, data)) { table.Rows.Clear(); if (IsRequired(type, requiredtables, alias)) data.CopyTo(table); } } protected void Load(CoreTable data, IEnumerable requiredtables, string? alias = null) { Load(typeof(TType), data, requiredtables, alias); } protected void Load(IEnumerable items, IEnumerable requiredtables, string? alias = null) where TType : notnull { CheckTable(alias); var name = TableName(alias); var table = _tables[name].Table; table.Rows.Clear(); if (IsRequired(requiredtables)) foreach (var item in items) { table.LoadRow(item); } } private void CheckTable(string? alias = null) { CheckTable(typeof(TType), alias); } protected static string TableName(string? alias = null) { return TableName(typeof(TType), alias); } public class DataModelTable { private bool shouldLoad; public DataModelTable(Type? type, CoreTable table, bool isDefault, IFilter? filter, IColumns? columns, bool shouldLoad = true) { Type = type; Table = table; IsDefault = isDefault; Filter = filter; Columns = columns; ShouldLoad = shouldLoad; } public Type? Type { get; } public CoreTable Table { get; set; } public bool IsDefault { get; set; } public IFilter? Filter { get; set; } public IColumns? Columns { get; set; } public bool ShouldLoad { get => shouldLoad && Type != null; set { shouldLoad = value; } } } private interface IDataModelLoadTable : IDataModelTable { public IFilter? GetFilter(); public IQueryDef GetQueryDef(); public void LoadData(CoreTable data); } public class DataModelLoadTable : IDataModelTable, IDataModelLoadTable where T : Entity, IRemotable, IPersistent, new() { public string Name { get; set; } public Type? Type => typeof(T); public DataModel Model { get; set; } public CoreTable Data { get; private set; } /// /// Set to if this table should be included in lists of default required tables. Thus, this is not used for the actual loading of data; use for that. /// public bool IsDefault { get; set; } public bool IsRequired { get; set; } public Filter Filter { get; set; } public Columns Columns { get; set; } public IEnumerable ChildTables => Model._relationships.Where(x => x.ParentTable == Name).Select(x => Model._tables[x.ChildTable]); public DataModelLoadTable(DataModel model, string name, Filter filter, Columns columns) { Model = model; Name = name; Filter = filter; Columns = columns; } public IDataModelTable ChildTable(string alias) { var name = TableName(alias); var relation = Model._relationships.Where(x => x.ParentTable == Name && x.ChildTable == alias).FirstOrDefault(); if(relation != null && Model._tables.TryGetValue(name, out var table)) { return table; } throw new TableDoesNotExistException(name); } public IDataModelTable ChildTable(string? alias = null) { IDataModelTable table; if (alias != null) { table = ChildTable(alias); if(table.Type != typeof(TChild)) { throw new TableDoesNotExistException(TableName(alias)); } return table; } var relations = ChildTables.Where(x => x.Type == typeof(TChild)).ToList(); if(relations.Count == 1) { return relations[0]; } var childName = TableName(alias); var single = relations.Where(x => x.Name == childName).SingleOrDefault() ?? throw new AmbiguousTableException(); return single; } public void Load() { Data = Client.Query(GetQueryDef()); } public Filter? GetFilter() { var relation = Model._relationships.Where(x => x.ChildTable == Name).FirstOrDefault(); if(relation != null && Model._tables[relation.ParentTable] is IDataModelLoadTable loadTable && loadTable.Type is Type parentType) { var subFilter = new Filter(relation.ChildColumnAsPropertyName()); subFilter.Operator = Operator.InQuery; subFilter.Value = SubQuery.Create(parentType, loadTable.GetFilter(), Column.Create(parentType, relation.ParentColumnAsPropertyName())); return Filters.Combine(Filter, subFilter); } return Filter; } IFilter? IDataModelLoadTable.GetFilter() => GetFilter(); public QueryDef GetQueryDef() { return new QueryDef(GetFilter(), Columns, LookupFactory.DefineSort()); } IQueryDef IDataModelLoadTable.GetQueryDef() => GetQueryDef(); public void LoadData(CoreTable data) { Data = data; } } #region New Load Methods public virtual void LoadModel(IEnumerable? requiredTables) { var requiredTablesList = requiredTables != null ? requiredTables.ToList() : new List(); CheckRequiredTables(requiredTablesList); var args = new CancelEventArgs(); OnBeforeLoad?.Invoke(args); if (!args.Cancel) BeforeLoad(requiredTablesList); var queries = new Dictionary(); foreach (var (key, table) in _tables) { if(requiredTables is null || requiredTablesList.Contains(key)) { if(table is IDataModelLoadTable loadTable) { queries[key] = loadTable.GetQueryDef(); } else { table.Load(); } } } var results = Client.QueryMultiple(queries); foreach (var (key, data) in results) if (_tables.TryGetValue(key, out var table) && table is IDataModelLoadTable loadTable) loadTable.LoadData(data); else Logger.Send(LogType.Error, "", $"QueryMultiple returned table with key {key}, which is not in the data model!"); args = new CancelEventArgs(); OnAfterLoad?.Invoke(args); if (!args.Cancel) AfterLoad(requiredTablesList); } public void LoadModel() { LoadModel(DefaultTableNames); } #endregion #region Non-Generic Stuff public bool IsChildTable(string tableName) { return _relationships.Any(x => x.ChildTable == tableName); } public static string TableName(string alias) => alias; public static string TableName(Type? type, string? alias = null) { return string.IsNullOrWhiteSpace(alias) ? (type ?? throw new Exception("No type or alias given!")).EntityName().Split('.').Last() : alias; } private void CheckTable(Type? type, string? alias = null) { var name = TableName(type, alias); if (!_tables.ContainsKey(name)) throw new TableDoesNotExistException(name); } public void AddTable(Type? type, CoreTable table, bool isdefault = false, string? alias = null) { var name = TableName(type, alias); if (!_tables.ContainsKey(name)) _tables[name] = new DataModelTable(type, table, isdefault, null, null, false); else throw new Exception(string.Format("[{0}] already exists in this data model!", name)); } public void SetTableData(Type type, CoreTable tableData, string? alias = null) { var name = TableName(type, alias); if (_tables.TryGetValue(name, out var table)) { table.Table = tableData; table.ShouldLoad = false; } else { throw new Exception(string.Format("[{0}] does not exist in this data model!", name)); } } public void LinkTable(Type parenttype, string parentcolumn, string childalias, string childcolumn, string? parentalias = null, bool isLookup = false) => LinkTable(parenttype, parentcolumn, null, childcolumn, parentalias, childalias, isLookup); public void LinkTable(Type parenttype, string parentcolumn, Type? childtype, string childcolumn, string? parentalias = null, string? childalias = null, bool isLookup = false) { CheckTable(parenttype, parentalias); CheckTable(childtype, childalias); var relationship = new DataModelRelationship( TableName(parenttype, parentalias), parentcolumn, TableName(childtype, childalias), childcolumn, isLookup ); if (!_relationships.Any(x => x.ParentTable.Equals(relationship.ParentTable) && x.ChildTable.Equals(relationship.ChildTable))) _relationships.Add(relationship); } public bool HasTable(Type type, string? alias = null) { var name = TableName(type, alias); return _tables.ContainsKey(name); } public bool HasTable(string? alias = null) => HasTable(typeof(T), alias); #endregion #region Adding & Linking Tables public void AddTable(string alias, CoreTable table, bool isdefault = false) => AddTable(null, table, isdefault, alias); public void AddTable(Filter? filter, Columns? columns, bool isdefault = false, string? alias = null, bool shouldLoad = true) { var name = TableName(alias); if (!_tables.ContainsKey(name)) { var table = new CoreTable(); if(columns != null) { table.LoadColumns(columns); } else { table.LoadColumns(typeof(TType)); } _tables[name] = new DataModelTable(typeof(TType), table, isdefault, filter, columns, shouldLoad); } } public void AddChildTable(Expression> parentcolumn, Expression> childcolumn, Filter? filter = null, Columns? columns = null, bool isdefault = false, string? parentalias = null, string? childalias = null) { CheckTable(parentalias); AddTable(filter, columns, isdefault, childalias); LinkTable(parentcolumn, childcolumn, parentalias, childalias, false); } public void AddLookupTable(Expression> sourcecolumn, Expression> lookupcolumn, Filter? filter = null, Columns? columns = null, bool isdefault = false, string? sourcealias = null, string? lookupalias = null) { CheckTable(sourcealias); AddTable(filter, columns, isdefault, lookupalias); LinkTable(sourcecolumn, lookupcolumn, sourcealias, lookupalias, true); } public CoreTable GetTable(string? alias = null) { CheckTable(alias); var name = TableName(alias); return _tables[name].Table; } public DataModelTable GetDataModelTable(string name) { return _tables[name]; } public DataModelTable GetDataModelTable(string? alias = null) { CheckTable(alias); var name = TableName(alias); return _tables[name]; } protected bool IsRequired(IEnumerable requiredtables, string? alias = null) { var name = TableName(alias); return requiredtables == null || requiredtables.Contains(name); } protected bool IsRequired(Type type, IEnumerable requiredtables, string? alias = null) { var name = TableName(type, alias); return requiredtables == null || requiredtables.Contains(name); } public void LinkTable(Expression> parent, Expression> child, string? parentalias = null, string? childalias = null, bool isLookup = false) { CheckTable(parentalias); CheckTable(childalias); var relationship = new DataModelRelationship(parentalias, parent, childalias, child, isLookup); if (!_relationships.Any(x => x.ParentTable.Equals(relationship.ParentTable) && x.ChildTable.Equals(relationship.ChildTable))) _relationships.Add(relationship); //SetupIDs(relationship.ParentColumn); } #endregion #region Getting/Setting Table Data public Filter? GetFilter(string? alias = null) { var table = GetDataModelTable(alias); return table.Filter as Filter; } public Columns? GetColumns(string? alias = null) { var table = GetDataModelTable(alias); return table.Columns as Columns; } public IColumns? GetColumns(string alias) { var table = GetDataModelTable(alias); return table.Columns; } [Obsolete("Use SetColumns instead")] public void SetTableColumns(Columns columns, string? alias = null) => SetColumns(columns, alias); public void SetFilter(Filter? filter, string? alias = null) { var table = GetDataModelTable(alias); table.Filter = filter; } public void SetColumns(Columns? columns, string? alias = null) { var table = GetDataModelTable(alias); table.Columns = columns; } public void SetIsDefault(bool isDefault, string? alias = null) { var table = GetDataModelTable(alias); table.IsDefault = isDefault; } public void SetShouldLoad(bool shouldLoad, string? alias = null) { var table = GetDataModelTable(alias); table.ShouldLoad = shouldLoad; } #endregion #region Removing Tables private bool RemoveTable(Type type, string? alias = null) { var name = TableName(type, alias); return _tables.Remove(name); } public bool RemoveTable(string? alias = null) => RemoveTable(typeof(TType), alias); #endregion } public abstract class DataModel : DataModel, IDataModel where T : Entity, IRemotable, IPersistent, new() { public DataModel(Filter? filter, Columns? columns = null, SortOrder? sort = null) { Filter = filter; Columns = columns; Sort = sort; AddTable(filter, columns, true); AddChildTable(x => x.ID, x => x.EntityID); } public Filter? Filter { get; set; } public Columns? Columns { get; set; } public SortOrder? Sort { get; set; } } public class DataModelRelationship : IDataModelRelationship { public DataModelRelationship(string parenttable, string parentcolumn, string childtable, string childcolumn, bool isLookup = false) { ParentTable = parenttable; ParentColumn = parentcolumn; ChildTable = childtable; ChildColumn = childcolumn; IsLookup = isLookup; } public string ChildColumn { get; } public string ChildTable { get; } public string ParentColumn { get; } public string ParentTable { get; } public bool IsLookup { get; } public DataRelation AsDataRelation() { string parentTable, parentColumn, childTable, childColumn; // Reverse the relationships if it is a lookup if (IsLookup) { parentTable = ChildTable; parentColumn = ChildColumn; childTable = ParentTable; childColumn = ParentColumn; } else { parentTable = ParentTable; parentColumn = ParentColumn; childTable = ChildTable; childColumn = ChildColumn; } var result = new DataRelation( string.Format("{0}_{1}_{2}_{3}", parentTable, parentColumn, childTable, childColumn), parentTable, childTable, new[] { parentColumn }, new[] { childColumn }, false ); result.RelationName = string.Format("{0}_{1}_{2}_{3}", parentTable, parentColumn, childTable, childColumn); return result; } public string ParentColumnAsPropertyName() { return ParentColumn; } public string ChildColumnAsPropertyName() { return ChildColumn; } } public class DataModelRelationship : IDataModelRelationship { public DataModelRelationship(string? parentalias, Expression> parent, string? childalias, Expression> child, bool isLookup) { ParentTable = string.IsNullOrWhiteSpace(parentalias) ? typeof(TParent).EntityName().Split('.').Last() : parentalias; Parent = parent; ChildTable = string.IsNullOrWhiteSpace(childalias) ? typeof(TChild).EntityName().Split('.').Last() : childalias; Child = child; IsLookup = isLookup; } public Expression> Child { get; } public Expression> Parent { get; } public string ChildTable { get; } public string ChildColumn => CoreUtils.GetFullPropertyName(Child, ".").Replace('.', '_'); public string ParentTable { get; } public string ParentColumn => CoreUtils.GetFullPropertyName(Parent, ".").Replace('.', '_'); public bool IsLookup { get; } public string ParentColumnAsPropertyName() { return CoreUtils.GetFullPropertyName(Parent, "."); } public string ChildColumnAsPropertyName() { return CoreUtils.GetFullPropertyName(Child, "."); } public DataRelation AsDataRelation() { string parentTable, parentColumn, childTable, childColumn; // Reverse the relationships if it is a lookup if (IsLookup) { parentTable = ChildTable; parentColumn = ChildColumn; childTable = ParentTable; childColumn = ParentColumn; } else { parentTable = ParentTable; parentColumn = ParentColumn; childTable = ChildTable; childColumn = ChildColumn; } var result = new DataRelation( string.Format("{0}_{1}_{2}_{3}", parentTable, parentColumn, childTable, childColumn), parentTable, childTable, new[] { parentColumn }, new[] { childColumn }, false ); result.RelationName = string.Format("{0}_{1}_{2}_{3}", parentTable, parentColumn, childTable, childColumn); return result; } } }