using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Data.Common; using System.Drawing.Design; using System.Linq; using System.Reflection; using FastReport.Data.JsonConnection; using FastReport.Utils; namespace FastReport.Data { /// /// The base class for all data connection components such as . /// /// This example shows how to add a new MS Access connection to the report. /// /// Report report1; /// MsAccessDataConnection conn = new MsAccessDataConnection(); /// conn.DataSource = @"c:\data.mdb"; /// report1.Dictionary.Connections.Add(conn); /// conn.CreateAllTables(); /// /// public abstract partial class DataConnectionBase : DataComponentBase, IParent { #region Fields private DataSet dataSet; private TableCollection tables; private bool isSqlBased; private bool canContainProcedures; private string connectionString; private string connectionStringExpression; private bool loginPrompt; private int commandTimeout; private string lastConnectionString; #endregion #region Properties /// /// Gets an internal DataSet object that contains all data tables. /// [Browsable(false)] public DataSet DataSet { get { if (dataSet == null) dataSet = CreateDataSet(); return dataSet; } } /// /// Gets a collection of data tables in this connection. /// /// /// To add a table to the connection, you must either create a new TableDataSource and add it /// to this collection or call the method which will add /// all tables available in the database. /// [Browsable(false)] public TableCollection Tables { get { return tables; } } /// /// Gets or sets a connection string that contains all connection parameters. /// /// /// To modify some parameter of the connection, use respective /// ConnectionStringBuilder class. /// Security note: the connection string may contain a user name/password. /// This information is stored in a report file. By default, it is crypted using the standard /// FastReport's password. Since FastReport's source code is available to anyone who paid for it, /// it may be insecure to use the standard password. For more security, you should use own /// password. To do this, specify it in the Crypter.DefaultPassword property. /// /// This example demonstrates how to change a connection string: /// /// OleDbConnectionStringBuilder builder = new OleDbConnectionStringBuilder(oleDbConnection1.ConnectionString); /// builder.PersistSecurityInfo = false; /// oleDbConnection1.ConnectionString = builder.ToString(); /// /// [Category("Data")] [Browsable(true)] public string ConnectionString { get { if (Report != null && Report.IsRunning && !String.IsNullOrEmpty(ConnectionStringExpression)) { string value = (string)Report.Calc(ConnectionStringExpression); if (!string.IsNullOrEmpty(value)) return value; } return connectionString; } set { SetConnectionString(Crypter.DecryptString(value)); } } /// /// Gets or sets an expression that returns a connection string. /// /// /// Use this property to set the connection string dynamically. /// The recommended way to do this is to define a report parameter. You can do this in the /// "Data" window. Once you have defined the parameter, you can use it to pass a value /// to the connection. Set the ConnectionStringExpression property of the /// connection object to the report parameter's name (so it will look like [myReportParam]). /// To pass a value to the report parameter from your application, use the /// method. /// /// Once you set value for this property, the property will be ignored /// when report is run. /// /// [Category("Data")] [Editor("FastReport.TypeEditors.ExpressionEditor, FastReport", typeof(UITypeEditor))] public string ConnectionStringExpression { get { return connectionStringExpression; } set { connectionStringExpression = value; } } /// /// Gets or sets a value indicates if this connection is SQL-based. /// [Browsable(false)] public bool IsSqlBased { get { return isSqlBased; } set { isSqlBased = value; } } /// /// Gets or sets a value indicates if this connection can contain procedures. /// [Browsable(false)] public bool CanContainProcedures { get { return canContainProcedures; } set { canContainProcedures = value; } } /// /// Gets or sets a value indicating whether a login dialog appears immediately before opening a connection. /// /// /// Set LoginPrompt to true to provide login dialog when establishing a connection. If this /// property is false (by default), you should provide login information (user name and password) /// in the property. Though that property is stored in a crypted form, /// this may be insecure. /// Another way to pass login information to the connection is to use /// property that is bound to the report parameter. In that /// case you supply the entire connection string from your application. /// [Category("Data")] [DefaultValue(false)] public bool LoginPrompt { get { return loginPrompt; } set { loginPrompt = value; } } /// /// Gets or sets the command timeout, in seconds. /// [Category("Data")] [DefaultValue(30)] public int CommandTimeout { get { return commandTimeout; } set { commandTimeout = value; } } internal List TablesStructure { get; set; } #endregion #region Private Methods private void GetDBObjectNames(string name, List list) { DataTable schema = null; DbConnection conn = GetConnection(); try { OpenConnection(conn); schema = conn.GetSchema("Tables", new string[] { null, null, null, name }); } finally { DisposeConnection(conn); } foreach (DataRow row in schema.Rows) { list.Add(row["TABLE_NAME"].ToString()); } } private string PrepareSelectCommand(string selectCommand, string tableName, DbConnection connection) { if (String.IsNullOrEmpty(selectCommand)) { selectCommand = "select * from " + QuoteIdentifier(tableName, connection); } return selectCommand; } private TableDataSource FindTableDataSource(DataTable table) { foreach (TableDataSource c in Tables) { if (c.Table == table) return c; } return null; } #endregion #region Protected Methods /// protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing) DisposeDataSet(); } /// /// Initializes a DataSet instance. /// /// The DataSet object. /// /// This method is used to support FastReport infrastructure. You don't need to use it. /// protected virtual DataSet CreateDataSet() { DataSet dataSet = new DataSet(); dataSet.EnforceConstraints = false; return dataSet; } /// /// Disposes a DataSet. /// /// /// This method is used to support FastReport infrastructure. You don't need to use it. /// protected void DisposeDataSet() { if (dataSet != null) dataSet.Dispose(); dataSet = null; } /// /// Sets the connection string. /// /// New connection string. /// /// Use this method if you need to perform some actions when the connection string is set. /// protected virtual void SetConnectionString(string value) { connectionString = value; } /// /// Gets a connection string that contains username and password specified. /// /// User name. /// Password. /// /// Override this method to pass login information to the connection. Typical implementation /// must get the existing , merge specified login information into it /// and return the new value. /// protected virtual string GetConnectionStringWithLoginInfo(string userName, string password) { return ConnectionString; } #endregion #region IParent Members /// public bool CanContain(Base child) { return child is TableDataSource; } /// public void GetChildObjects(ObjectCollection list) { foreach (TableDataSource c in Tables) { list.Add(c); } } /// public void AddChild(Base child) { Tables.Add(child as TableDataSource); } /// public void RemoveChild(Base child) { Tables.Remove(child as TableDataSource); } /// public int GetChildOrder(Base child) { // we don't need to handle database objects' order. return 0; } /// public void SetChildOrder(Base child, int order) { // do nothing } /// public void UpdateLayout(float dx, float dy) { // do nothing } #endregion #region Public Methods /// /// Fills the collection with all tables available in the database. /// /// /// This method does not read the table data; to do this, call the /// method of each table. /// public void CreateAllTables() { CreateAllTables(true); } /// /// Fills the collection with all tables available in the database. /// /// Set to true to initialize each table's schema. public virtual void CreateAllTables(bool initSchema) { List tableNames = new List(); tableNames.AddRange(GetTableNames()); FilterTables(tableNames); // remove tables with tablename that does not exist in the connection. for (int i = 0; i < Tables.Count; i++) { TableDataSource table = Tables[i]; // skip queries and procedures if (!String.IsNullOrEmpty(table.SelectCommand) || table is ProcedureDataSource) continue; bool found = false; foreach (string tableName in tableNames) { if (String.Compare(table.TableName, tableName, true) == 0) { found = true; break; } } // table name not found in actual tablenames. It may happen if we have edited the connection. if (!found) { table.Dispose(); i--; } } int tableNumber = 0; // now create tables that are not created yet. foreach (string tableName in tableNames) { bool found = false; foreach (TableDataSource table in Tables) { if (String.Compare(table.TableName, tableName, true) == 0) { found = true; break; } } if (!found) { var table = new TableDataSource(); // Enabled = false is used to ensure that when creating a connection, // none of the tables in the list of tables will be selected by default table.Enabled = false; string fixedTableName = tableName.Replace(".", "_").Replace("[", "").Replace("]", "").Replace("\"", ""); if (Report != null) { table.Name = Report.Dictionary.CreateUniqueName(fixedTableName); table.Alias = Report.Dictionary.CreateUniqueAlias(table.Alias); } else table.Name = fixedTableName; table.TableName = tableName; table.Connection = this; if (TablesStructure != null) { table.Enabled = TablesStructure[tableNumber].Properties.Where(prop => prop.Key == "Enabled").Select(res => res.Value).First() == "true"; } Tables.Add(table); tableNumber++; } } // init table schema if (initSchema) { foreach (TableDataSource table in Tables) { table.InitSchema(); } } } /// /// Fills the collection with all procedures available in the database. /// public virtual void CreateAllProcedures() { List procedureNames = new List(); procedureNames.AddRange(GetProcedureNames()); FilterTables(procedureNames); // remove procedures that does not exist in the connection. for (int i = 0; i < Tables.Count; i++) { var proc = Tables[i] as ProcedureDataSource; if (proc == null) continue; bool found = false; foreach (string procName in procedureNames) { if (String.Compare(proc.TableName, procName, true) == 0) { found = true; break; } } // proc name not found in actual proc names. It may happen if we have edited the connection. if (!found) { proc.Dispose(); i--; } } // now create procedures that are not created yet. foreach (string procName in procedureNames) { bool found = false; foreach (TableDataSource table in Tables) { if (String.Compare(table.TableName, procName, true) == 0) { found = true; break; } } if (!found) { var proc = CreateProcedure(procName); proc.Enabled = false; string fixedName = procName.Replace(".", "_").Replace("[", "").Replace("]", "").Replace("\"", ""); if (Report != null) { proc.Name = Report.Dictionary.CreateUniqueName(fixedName); proc.Alias = Report.Dictionary.CreateUniqueAlias(proc.Alias); } else proc.Name = fixedName; proc.TableName = procName; proc.Connection = this; Tables.Add(proc); } } } /// /// Create the stored procedure. /// public virtual TableDataSource CreateProcedure(string tableName) { ProcedureDataSource table = new ProcedureDataSource(); table.Enabled = true; table.SelectCommand = tableName; DbConnection conn = GetConnection(); try { OpenConnection(conn); var schemaParameters = conn.GetSchema("PROCEDURE_PARAMETRS", new string[] { null, null, tableName }); foreach (DataRow row in schemaParameters.Rows) { ParameterDirection direction = ParameterDirection.Input; switch (row["PARAMETER_MODE"].ToString()) { case "IN": direction = ParameterDirection.Input; table.Enabled = false; break; case "INOUT": direction = ParameterDirection.InputOutput; table.Enabled = false; break; case "OUT": direction = ParameterDirection.Output; break; } table.Parameters.Add(new ProcedureParameter() { Name = row["PARAMETER_NAME"].ToString(), DataType = Convert.ToInt32(row["DATA_TYPE"].ToString()), Direction = direction }); } } finally { DisposeConnection(conn); } return table; } /// /// Creates the relations between tables. Applies to XmlDataConnection only. /// public virtual void CreateRelations() { if (Report != null) { foreach (DataRelation relation in DataSet.Relations) { Relation rel = new Relation(); rel.Name = relation.RelationName; rel.ParentDataSource = FindTableDataSource(relation.ParentTable); rel.ChildDataSource = FindTableDataSource(relation.ChildTable); string[] parentColumns = new string[relation.ParentColumns.Length]; string[] childColumns = new string[relation.ChildColumns.Length]; for (int i = 0; i < relation.ParentColumns.Length; i++) { parentColumns[i] = relation.ParentColumns[i].Caption; } for (int i = 0; i < relation.ChildColumns.Length; i++) { childColumns[i] = relation.ChildColumns[i].Caption; } rel.ParentColumns = parentColumns; rel.ChildColumns = childColumns; if (Report.Dictionary.Relations.FindEqual(rel) == null) Report.Dictionary.Relations.Add(rel); else rel.Dispose(); } } } /// /// Gets an array of table names available in the database. /// /// An array of strings. public virtual string[] GetTableNames() { List list = new List(); GetDBObjectNames("TABLE", list); GetDBObjectNames("VIEW", list); return list.ToArray(); } /// /// Gets an array of table names available in the database. /// /// An array of strings. public virtual string[] GetProcedureNames() { List list = new List(); DataTable schema = null; DbConnection conn = GetConnection(); if (conn != null) { try { OpenConnection(conn); schema = conn.GetSchema("PROCEDURE"); foreach (DataRow row in schema.Rows) { list.Add(row["PROCEDURE_NAME"].ToString()); } } finally { DisposeConnection(conn); } } return list.ToArray(); } /// /// Returns a type of connection. /// /// Type instance. /// /// You should override this method if you developing a new connection component. /// If your connection component does not use data connection, you need to override /// the and methods instead. /// /// Here is the example of this method implementation: /// /// public override Type GetConnectionType() /// { /// return typeof(OleDbConnection); /// } /// /// public virtual Type GetConnectionType() { return null; } /// /// Returns a connection object. /// /// The DbConnection instance. /// Either creates a new DbConnection instance of type provided by the /// method, or returns the application connection if set /// in the Config.DesignerSettings.ApplicationConnection. public DbConnection GetConnection() { Type connectionType = GetConnectionType(); if (connectionType != null) { DbConnection connection = GetDefaultConnection(); if (connection != null) return connection; // create a new connection object connection = Activator.CreateInstance(connectionType) as DbConnection; connection.ConnectionString = ConnectionString; return connection; } return null; } /// /// Opens a specified connection object. /// /// Connection to open. /// /// Use this method to open a connection returned by the method. /// This method displays a login dialog if your connection has the /// property set to true. Once you have entered an user name and password in /// this dialog, it will remeber the entered values and will not used anymore in this report session. /// public void OpenConnection(DbConnection connection) { if (connection.State == ConnectionState.Open) return; if (!String.IsNullOrEmpty(lastConnectionString)) { // connection string is already provided, use it and skip other logic. connection.ConnectionString = lastConnectionString; } else { // try the global DatabaseLogin event string oldConnectionString = ConnectionString; DatabaseLoginEventArgs e = new DatabaseLoginEventArgs(ConnectionString); Config.ReportSettings.OnDatabaseLogin(this, e); // that event may do the following: // - modify the ConnectionString // - modify the username/password // - there is no event handler attached to the event, so it does nothing. if (oldConnectionString != e.ConnectionString) { // the connection string was modified. Set the FLastConnectionString to use it next time silently. lastConnectionString = e.ConnectionString; } else { if (!String.IsNullOrEmpty(e.UserName) || !String.IsNullOrEmpty(e.Password)) { // the username/password was modified. Get new connection string lastConnectionString = GetConnectionStringWithLoginInfo(e.UserName, e.Password); } else if (LoginPrompt) { ShowLoginForm(lastConnectionString); } } // update the connection if it's not done yet if (!String.IsNullOrEmpty(lastConnectionString)) connection.ConnectionString = lastConnectionString; } connection.Open(); Config.ReportSettings.OnAfterDatabaseLogin(this, new AfterDatabaseLoginEventArgs(connection)); } /// /// Disposes a connection. /// /// The connection to dispose. public void DisposeConnection(DbConnection connection) { if (ShouldNotDispose(connection)) return; if (connection != null) connection.Dispose(); } /// /// Returns a object that is specific to this connection. /// /// The SQL command used to fetch a table data rows. /// The connection object. /// The select command parameters. /// The DbDataAdapter object. /// /// You should override this method if you are developing a new connection component. In this method, /// you need to create the adapter and set its SelectCommand's parameters. /// If your connection does not use data adapter, you need to override /// the and methods instead. /// /// Here is the example of this method implementation: /// /// public override DbDataAdapter GetAdapter(string selectCommand, DbConnection connection, /// CommandParameterCollection parameters) /// { /// OleDbDataAdapter adapter = new OleDbDataAdapter(selectCommand, connection as OleDbConnection); /// foreach (CommandParameter p in parameters) /// { /// OleDbParameter parameter = adapter.SelectCommand.Parameters.Add(p.Name, (OleDbType)p.DataType, p.Size); /// parameter.Value = p.Value; /// } /// return adapter; /// } /// /// public virtual DbDataAdapter GetAdapter(string selectCommand, DbConnection connection, CommandParameterCollection parameters) { return null; } /// /// Gets the type of parameter that is specific to this connection. /// /// The parameter's type. /// /// This property is used in the report designer to display available data types when you edit the /// connection parameters. For example, the type of OleDbConnection parameter is a OleDbType. /// public virtual Type GetParameterType() { return null; } /// /// Quotes the specified DB identifier such as table name or column name. /// /// Identifier to quote. /// The opened DB connection. /// The quoted identifier. public abstract string QuoteIdentifier(string value, DbConnection connection); /// /// Fills the table schema. /// /// DataTable to fill. /// The SQL select command. /// SQL parameters. /// /// Usually you don't need to use this method. Internally it uses the and /// methods to fill the table schema. If you create own connection component /// that does not use nor connection or adapter, then you need to override this method. /// public virtual void FillTableSchema(DataTable table, string selectCommand, CommandParameterCollection parameters) { DbConnection conn = GetConnection(); try { OpenConnection(conn); TableDataSource dataSource = FindTableDataSource(table); // prepare select command if (!(dataSource is ProcedureDataSource)) selectCommand = PrepareSelectCommand(selectCommand, table.TableName, conn); // read the table schema using (DbDataAdapter adapter = GetAdapter(selectCommand, conn, parameters)) { adapter.SelectCommand.CommandType = dataSource is ProcedureDataSource ? CommandType.StoredProcedure : CommandType.Text; adapter.SelectCommand.CommandTimeout = CommandTimeout; adapter.FillSchema(table, SchemaType.Source); } foreach (Column column in dataSource.Columns) { if (!column.Enabled) table.Columns.Remove(column.Name); } } finally { DisposeConnection(conn); } } /// /// Fills the table data. /// /// DataTable to fill. /// The SQL select command. /// SQL parameters. /// /// Usually you don't need to use this method. Internally it uses the and /// methods to fill the table data. If you create own connection component /// that does not use nor connection or adapter, then you need to override this method. /// public virtual void FillTableData(DataTable table, string selectCommand, CommandParameterCollection parameters) { DbConnection conn = GetConnection(); try { OpenConnection(conn); TableDataSource dataSource = FindTableDataSource(table); // prepare select command if (!(dataSource is ProcedureDataSource)) selectCommand = PrepareSelectCommand(selectCommand, table.TableName, conn); // read the table using (DbDataAdapter adapter = GetAdapter(selectCommand, conn, parameters)) { adapter.SelectCommand.CommandType = dataSource is ProcedureDataSource ? CommandType.StoredProcedure : CommandType.Text; adapter.SelectCommand.CommandTimeout = CommandTimeout; table.Clear(); adapter.Fill(table); // copy output parameter values foreach (DbParameter dp in adapter.SelectCommand.Parameters) { if (dp.Direction != ParameterDirection.Input) { foreach (CommandParameter cp in parameters) { if (cp.Name == dp.ParameterName) { cp.Value = dp.Value; break; } } } } } } finally { DisposeConnection(conn); } } /// /// Creates table. /// For internal use only. /// public virtual void CreateTable(TableDataSource source) { if (source.Table == null) { source.Table = new DataTable(source.TableName); DataSet.Tables.Add(source.Table); } } internal virtual void FillTable(TableDataSource source) { if (source.Table != null) { bool parametersChanged = false; foreach (CommandParameter par in source.Parameters) { object value = par.Value; if (!Object.Equals(value, par.LastValue)) { par.LastValue = value; parametersChanged = true; } } if (source.ForceLoadData || source.Table.Rows.Count == 0 || parametersChanged) FillTableData(source.Table, source.SelectCommand, source.Parameters); } } /// /// Deletes table. /// For internal use only. /// public virtual void DeleteTable(TableDataSource source) { if (source.Table != null) { if (dataSet != null) dataSet.Tables.Remove(source.Table); source.Table.Dispose(); source.Table = null; } } /// /// Clone table. /// For internal use only. /// public virtual void Clone() { XmlItem item = new XmlItem(); using (FRWriter writer = new FRWriter(item)) { writer.SerializeTo = SerializeTo.Clipboard; writer.BlobStore = new BlobStore(false); writer.Write(this); } using (FRReader reader = new FRReader(Report, item)) { reader.DeserializeFrom = SerializeTo.Clipboard; reader.BlobStore = new BlobStore(false); var connection = Activator.CreateInstance(this.GetType()) as DataConnectionBase; connection.Parent = this.Parent; connection.SetReport(Report); reader.Read(connection); connection.CreateUniqueName(); foreach (TableDataSource table in connection.Tables) table.CreateUniqueName(); Report.Dictionary.AddChild(connection); } } protected void CreateUniqueNames(DataConnectionBase copyTo) { int i = 1; string s; do { s = this.Alias + i.ToString(); i++; } while (Report.Dictionary.FindByAlias(s) != null); copyTo.Alias = s; copyTo.Name = s; } /// public override void Serialize(FRWriter writer) { writer.ItemName = ClassName; if (Name != "") writer.WriteStr("Name", Name); if (Restrictions != Restrictions.None) writer.WriteValue("Restrictions", Restrictions); if (!String.IsNullOrEmpty(ConnectionString)) writer.WriteStr("ConnectionString", Crypter.EncryptString(ConnectionString)); if (!String.IsNullOrEmpty(ConnectionStringExpression)) writer.WriteStr("ConnectionStringExpression", ConnectionStringExpression); if (LoginPrompt) writer.WriteBool("LoginPrompt", true); if (CommandTimeout != 30) writer.WriteInt("CommandTimeout", CommandTimeout); SerializeDesignExt(writer); if (writer.SaveChildren) { foreach (TableDataSource c in Tables) { if (c.Enabled) writer.Write(c); } } } /// public override string[] GetExpressions() { return new string[] { ConnectionStringExpression }; } partial void SetConnectionStringBrowsableAttribute(); #endregion /// /// Initializes a new instance of the class with default settings. /// public DataConnectionBase() { tables = new TableCollection(this); connectionString = ""; connectionStringExpression = ""; IsSqlBased = true; canContainProcedures = false; commandTimeout = 30; SetFlags(Flags.CanEdit, true); SetConnectionStringBrowsableAttribute(); } partial void SerializeDesignExt(FRWriter writer); } }