//****************************************************************************************************** // IsamDataFileBase.cs - Gbtc // // Copyright © 2012, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/eclipse-1.0.php // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 03/08/2007 - Pinal C. Patel // Original version of source code generated. // 11/30/2007 - Pinal C. Patel // Modified the "design time" check in EndInit() method to use LicenseManager.UsageMode property // instead of DesignMode property as the former is more accurate than the latter. // 09/19/2008 - J. Ritchie Carroll // Converted to C#. // 10/28/2008 - Pinal C. Patel // Edited code comments. // 12/04/2008 - J. Ritchie Carroll // Modified class an example to use new ISupportBinaryImage. // 05/12/2009 - Pinal C. Patel // Optimized Read() for better memory management by using "yield return". // 05/19/2009 - Pinal C. Patel // Implemented the IProvideStatus interface. // 07/02/2009 - Pinal C. Patel // Modified state alterning properties to reopen the file when changed. // 08/10/2009 - Pinal C. Patel // Modified Write() to write empty intermediate records that are missing to avoid garbage data // for the missing intermediate records when records are being written to disk directly. // 09/14/2009 - Stephen C. Wills // Added new header and license agreement. // 09/16/2009 - Pinal C. Patel // Modified Save() to flush buffered data to disk. // Removed MinimumRecordCount property - not very useful. // 09/17/2009 - Pinal C. Patel // Added exception handling to event handlers. // 12/04/2009 - Pinal C. Patel // Fixed thread synchronization bug in ReadFromDisk(). // 04/07/2011 - Pinal C. Patel // Removed inheritance from Component class to allow for serialization. // 11/23/2011 - J. Ritchie Carroll // Modified to support buffer optimized ISupportBinaryImage. // 12/06/2011 - Pinal C. Patel // Updated to instantiate a FileSystemWatcher object that watches for changes to the file content // only if needed to avoid a bug introduced in .NET 4.0 that causes memory leak. // 12/14/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Configuration; using System.IO; using System.Text; using System.Threading; using System.Timers; using System.Xml.Serialization; using GSF.Configuration; using GSF.Parsing; using Timer = System.Timers.Timer; namespace GSF.IO { /// /// An abstract class that defines the read/write capabilities for ISAM (Indexed Sequential Access Method) file. /// /// /// of the records the file contains. This must implement the interface. /// /// /// /// This ISAM implementation keeps all the records in memory, so it may not be suitable for very large files. Since data is stored /// in memory using a list, the maximum number of possible supported records will be 2,147,483,647 (i.e., Int32.MaxValue). /// /// /// See http://en.wikipedia.org/wiki/ISAM for more information on ISAM files. /// /// /// /// This example shows a sample implementation of : /// /// using System; /// using System.Text; /// using GSF; /// using GSF.IO; /// using GSF.Parsing; /// /// class Program /// { /// static void Main(string[] args) /// { /// // Create a few test records. /// TestIsamFileRecord r1 = new TestIsamFileRecord(1); /// r1.Name = "TestRecord1"; /// r1.Value = double.MinValue; /// r1.Description = "Test record with minimum double value"; /// TestIsamFileRecord r2 = new TestIsamFileRecord(2); /// r2.Name = "TestRecord2"; /// r2.Value = double.MaxValue; /// r2.Description = "Test record with maximum double value"; /// /// // Open ISAM file. /// TestIsamFile testFile = new TestIsamFile(); /// testFile.FileName = "TestIsamFile.dat"; /// testFile.Open(); /// /// // Write test records. /// testFile.Write(r1.Index, r1); /// testFile.Write(r2.Index, r2); /// /// // Read test records. /// Console.WriteLine(testFile.Read(1)); /// Console.WriteLine(testFile.Read(2)); /// /// // Close ISAM file. /// testFile.Close(); /// /// Console.ReadLine(); /// } /// } /// /// class TestIsamFile : IsamDataFileBase<TestIsamFileRecord> /// { /// /// /// /// Size of a single file record. /// /// /// protected override int GetRecordSize() /// { /// return TestIsamFileRecord.RecordLength; /// } /// /// /// /// /// Creates a new empty file record. /// /// /// protected override TestIsamFileRecord CreateNewRecord(int id) /// { /// return new TestIsamFileRecord(id); /// } /// } /// /// class TestIsamFileRecord : ISupportBinaryImage /// { /// private int m_index; /// private string m_name; // 20 * 1 = 20 /// private double m_value; // 1 * 8 = 8 /// private string m_description; // 100 * 1 = 100 /// /// public const int RecordLength = 128; // Total = 128 /// /// public TestIsamFileRecord(int recordIndex) /// { /// m_index = recordIndex; /// /// Name = string.Empty; /// Value = double.NaN; /// Description = string.Empty; /// } /// /// /// /// /// 1-based index of the record. /// /// /// public int Index /// { /// get { return m_index; } /// } /// /// /// /// /// Name of the record. /// /// /// public string Name /// { /// get { return m_name; } /// set { m_name = value.TruncateRight(20).PadRight(20); } /// } /// /// /// /// /// Value of the record. /// /// /// public double Value /// { /// get { return m_value; } /// set { m_value = value; } /// } /// /// /// /// /// Description of the record. /// /// /// public string Description /// { /// get { return m_description; } /// set { m_description = value.TruncateRight(100).PadRight(100); } /// } /// /// /// /// /// Serialized record length. /// /// /// public int BinaryLength /// { /// get { return RecordLength; } /// } /// /// /// /// /// Serialized record data. /// /// /// public byte[] BinaryImage /// { /// get /// { /// // Serialize TestIsamFileRecord into byte array. /// byte[] image = new byte[RecordLength]; /// Buffer.BlockCopy(Encoding.ASCII.GetBytes(Name), 0, image, 0, 20); /// Buffer.BlockCopy(BitConverter.GetBytes(Value), 0, image, 20, 8); /// Buffer.BlockCopy(Encoding.ASCII.GetBytes(Description), 0, image, 28, 100); /// /// return image; /// } /// } /// /// /// /// /// Deserializes the record. /// /// /// public int Initialize(byte[] binaryImage, int startIndex, int length) /// { /// if (length >= RecordLength) /// { /// // Deserialize byte array into TestIsamFileRecord. /// Name = Encoding.ASCII.GetString(binaryImage, startIndex, 20); /// Value = BitConverter.ToDouble(binaryImage, startIndex + 20); /// Description = Encoding.ASCII.GetString(binaryImage, startIndex + 28, 100); /// } /// else /// throw new InvalidOperationException("Invalid record size, not enough data to deserialize record"); /// /// return RecordLength; /// } /// /// /// /// /// String representation of the record. /// /// /// public override string ToString() /// { /// return string.Format("Name: {0}, Value: {1}, Description: {2}", Name, Value, Description); /// } /// } /// /// public abstract class IsamDataFileBase : ISupportLifecycle, IProvideStatus, IPersistSettings where T : ISupportBinaryImage { #region [ Members ] // Constants /// /// Specifies the default value for the property. /// public const string DefaultFileName = "IsamDataFile.dat"; /// /// Specifies the default value for the property. /// public const FileAccess DefaultFileAccessMode = FileAccess.ReadWrite; /// /// Specifies the default value for the property. /// public const int DefaultAutoSaveInterval = -1; /// /// Specifies the default value for the property. /// public const bool DefaultLoadOnOpen = false; /// /// Specifies the default value for the property. /// public const bool DefaultSaveOnClose = false; /// /// Specifies the default value for the property. /// public const bool DefaultReloadOnModify = false; /// /// Specifies the default value for the property. /// public const bool DefaultPersistSettings = false; /// /// Specifies the default value for the property. /// public const string DefaultSettingsCategory = "IsamDataFile"; // Events /// /// Occurs when data is being read from disk into memory. /// public event EventHandler DataLoading; /// /// Occurs when data has been read from disk into memory. /// public event EventHandler DataLoaded; /// /// Occurs when data is being saved from memory onto disk. /// public event EventHandler DataSaving; /// /// Occurs when data has been saved from memory onto disk. /// public event EventHandler DataSaved; /// /// Occurs when file data on the disk is modified. /// public event EventHandler FileModified; /// /// Occurs when the class has been disposed. /// public event EventHandler Disposed; // Fields private string m_fileName; private FileAccess m_fileAccessMode; private int m_autoSaveInterval; private bool m_loadOnOpen; private bool m_saveOnClose; private bool m_reloadOnModify; private bool m_persistSettings; private string m_settingsCategory; private List m_fileRecords; private byte[] m_recordBuffer; private FileStream m_fileData; private readonly ManualResetEvent m_loadWaitHandle; private readonly ManualResetEvent m_saveWaitHandle; private readonly Timer m_autoSaveTimer; private FileSystemWatcher m_fileWatcher; private bool m_disposed; private bool m_initialized; #endregion #region [ Constructors ] /// /// Initializes a new instance of the class. /// protected IsamDataFileBase() { m_fileName = DefaultFileName; m_fileAccessMode = DefaultFileAccessMode; m_autoSaveInterval = DefaultAutoSaveInterval; m_loadOnOpen = DefaultLoadOnOpen; m_saveOnClose = DefaultSaveOnClose; m_reloadOnModify = DefaultReloadOnModify; m_persistSettings = DefaultPersistSettings; m_settingsCategory = DefaultSettingsCategory; m_loadWaitHandle = new ManualResetEvent(true); m_saveWaitHandle = new ManualResetEvent(true); m_autoSaveTimer = new Timer(); m_autoSaveTimer.Elapsed += m_autoSaveTimer_Elapsed; } /// /// Releases the unmanaged resources before the file is reclaimed by . /// ~IsamDataFileBase() { Dispose(false); } #endregion #region [ Properties ] /// /// Gets or sets the name of the file. /// /// /// Changing the when the file is open will cause the file to be re-opend. /// public string FileName { get { return m_fileName; } set { if (string.IsNullOrEmpty(value)) throw new ArgumentNullException("value"); m_fileName = value; ReOpen(); } } /// /// Gets or sets the value to use when opening the file. /// public FileAccess FileAccessMode { get { return m_fileAccessMode; } set { m_fileAccessMode = value; ReOpen(); } } /// /// Gets or sets the interval in milliseconds at which the records loaded in memory are to be persisted to disk. /// /// /// will be effective only if records have been loaded in memory either manually /// by calling the method or automatically by settings to true. /// public int AutoSaveInterval { get { return m_autoSaveInterval; } set { m_autoSaveInterval = value; ReOpen(); } } /// /// Gets or sets a boolean value that indicates whether records are to be loaded automatically in memory when /// the file is opened. /// public bool LoadOnOpen { get { return m_loadOnOpen; } set { m_loadOnOpen = value; ReOpen(); } } /// /// Gets or sets a boolean value that indicates whether records loaded in memory are to be persisted to disk /// when the file is closed. /// /// /// will be effective only if records have been loaded in memory either manually /// by calling the method or automatically by settings to true. /// public bool SaveOnClose { get { return m_saveOnClose; } set { m_saveOnClose = value; } } /// /// Gets or sets a boolean value that indicates whether records loaded in memory are to be re-loaded when the /// file is modified on disk. /// /// /// will be effective only if records have been loaded in memory either manually /// by calling the method or automatically by settings to true. /// public bool ReloadOnModify { get { return m_reloadOnModify; } set { m_reloadOnModify = value; ReOpen(); } } /// /// Gets or sets a boolean value that indicates whether the file settings are to be saved to the config file. /// [XmlIgnore] public bool PersistSettings { get { return m_persistSettings; } set { m_persistSettings = value; } } /// /// Gets or sets the category under which the file settings are to be saved to the config file if the /// property is set to true. /// /// The value being assigned is null or empty string. [XmlIgnore] public string SettingsCategory { get { return m_settingsCategory; } set { if (string.IsNullOrEmpty(value)) throw new ArgumentNullException("value"); m_settingsCategory = value; } } /// /// Gets or sets a boolean value that indicates whether the file is currently enabled. /// /// /// Setting to true will open the file if it is closed, setting /// to false will close the file if it is open. /// [XmlIgnore] public bool Enabled { get { return IsOpen; } set { if (value && !IsOpen) Open(); else if (!value && IsOpen) Close(); } } /// /// Gets a boolean value that indicates whether the file is open. /// public bool IsOpen { get { return ((object)m_fileData != null); } } /// /// Gets a boolean value that indicates whether the file data on disk is corrupt. /// public bool IsCorrupt { get { if (IsOpen) { long fileLength; lock (m_fileData) { fileLength = m_fileData.Length; } return (fileLength % m_recordBuffer.Length != 0); } else { throw new InvalidOperationException(string.Format("{0} \"{1}\" is not open", this.GetType().Name, m_fileName)); } } } /// /// Gets the approximate memory consumption (in KB) of the file. /// /// /// will be zero (0) unless records are loaded in memory. /// public long MemoryUsage { get { return RecordsInMemory * m_recordBuffer.Length / 1024; } } /// /// Gets the number of file records on the disk. /// public int RecordsOnDisk { get { if (IsOpen) { long fileLength; lock (m_fileData) { fileLength = m_fileData.Length; } return (int)(fileLength / (long)m_recordBuffer.Length); } else { throw new InvalidOperationException(string.Format("{0} \"{1}\" is not open", this.GetType().Name, m_fileName)); } } } /// /// Gets the number of file records loaded in memory. /// public int RecordsInMemory { get { int recordCount = 0; if ((object)m_fileRecords != null) { lock (m_fileRecords) { recordCount = m_fileRecords.Count; } } return recordCount; } } /// /// Gets the unique identifier of the file. /// public string Name { get { return m_settingsCategory; } } /// /// Gets the descriptive status of the file. /// public string Status { get { StringBuilder status = new StringBuilder(); status.Append(" File name: "); status.Append(FilePath.TrimFileName(FileName, 30)); status.AppendLine(); status.Append(" File state: "); status.Append(IsOpen ? "Open" : "Closed"); status.AppendLine(); status.Append(" File access mode: "); status.Append(FileAccessMode); status.AppendLine(); status.Append(" Data validity: "); status.Append(IsCorrupt ? "Invalid" : "Valid"); status.AppendLine(); status.Append(" Auto-save data: "); if (LoadOnOpen && AutoSaveInterval > 0 && !SaveOnClose) status.AppendFormat("Every {0}ms", AutoSaveInterval); if (LoadOnOpen && AutoSaveInterval > 0 && SaveOnClose) status.AppendFormat("Every {0}ms & File Close", AutoSaveInterval); if (!LoadOnOpen || (LoadOnOpen && AutoSaveInterval < 1 && !SaveOnClose)) status.Append("Never"); status.AppendLine(); status.Append(" Records on disk: "); status.Append(RecordsOnDisk); status.AppendLine(); status.Append(" Records in memory: "); status.Append(RecordsInMemory); status.AppendLine(); status.Append(" Memory usage (approx.): "); status.AppendFormat("{0} KB", MemoryUsage); status.AppendLine(); return status.ToString(); } } /// /// Gets the underlying of the file. /// /// /// Thread-safety Warning: A lock must be obtained on before accessing it. /// protected FileStream FileData { get { return m_fileData; } } #endregion #region [ Methods ] #region [ Abstract ] /// /// When overridden in a derived class, gets the size of a record (in bytes). /// /// Size of a record in bytes. protected abstract int GetRecordSize(); /// /// When overridden in a derived class, returns a new empty record. /// /// 1-based index of the new record. /// New empty record. protected abstract T CreateNewRecord(int recordIndex); #endregion /// /// Releases all the resources used by the file. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Initializes the file. /// /// /// is to be called by user-code directly only if the file is not consumed through the designer surface of the IDE. /// public void Initialize() { if (!m_initialized) { // Load settings from the config file. LoadSettings(); // Watch for changes to the file content. if (m_reloadOnModify) { // Create watcher only if needed to avoid a bug in .NET 4.0 that causes memory leak. // http://support.microsoft.com/kb/2628838 m_fileWatcher = new FileSystemWatcher(); m_fileWatcher.Path = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(m_fileName)); m_fileWatcher.Filter = FilePath.GetFileName(m_fileName); m_fileWatcher.Changed += m_fileWatcher_Changed; } m_recordBuffer = new byte[GetRecordSize()]; // Create buffer for reading records. m_initialized = true; // Initialize only once. } } /// /// Saves settings of the file to the config file if the property is set to true. /// /// has a value of null or empty string. public void SaveSettings() { if (m_persistSettings) { // Ensure that settings category is specified. if (string.IsNullOrEmpty(m_settingsCategory)) throw new ConfigurationErrorsException("SettingsCategory property has not been set"); // Save settings under the specified category. ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings[m_settingsCategory]; settings["FileName", true].Update(m_fileName); settings["FileAccessMode", true].Update(m_fileAccessMode); settings["AutoSaveInterval", true].Update(m_autoSaveInterval); settings["LoadOnOpen", true].Update(m_loadOnOpen); settings["SaveOnClose", true].Update(m_saveOnClose); settings["ReloadOnModify", true].Update(m_reloadOnModify); config.Save(); } } /// /// Loads saved settings of the file from the config file if the property is set to true. /// /// has a value of null or empty string. public void LoadSettings() { if (m_persistSettings) { // Ensure that settings category is specified. if (string.IsNullOrEmpty(m_settingsCategory)) throw new ConfigurationErrorsException("SettingsCategory property has not been set"); // Load settings from the specified category. ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings[m_settingsCategory]; settings.Add("FileName", m_fileName, "Name of the file including its path."); settings.Add("FileAccessMode", m_fileAccessMode, "Access mode (Read; Write; ReadWrite) to be used when opening the file."); settings.Add("AutoSaveInterval", m_autoSaveInterval, "Interval in milliseconds at which the file records loaded in memory are to be saved automatically to disk. Use -1 to disable automatic saving."); settings.Add("LoadOnOpen", m_loadOnOpen, "True if file records are to be loaded in memory when opened; otherwise False."); settings.Add("SaveOnClose", m_saveOnClose, "True if file records loaded in memory are to be saved to disk when file is closed; otherwise False."); settings.Add("ReloadOnModify", m_reloadOnModify, "True if file records loaded in memory are to be re-loaded when file is modified on disk; otherwise False."); FileName = settings["FileName"].ValueAs(m_fileName); FileAccessMode = settings["FileAccessMode"].ValueAs(m_fileAccessMode); AutoSaveInterval = settings["AutoSaveInterval"].ValueAs(m_autoSaveInterval); LoadOnOpen = settings["LoadOnOpen"].ValueAs(m_loadOnOpen); SaveOnClose = settings["SaveOnClose"].ValueAs(m_saveOnClose); ReloadOnModify = settings["ReloadOnModify"].ValueAs(m_reloadOnModify); } } /// /// Opens the file. /// public void Open() { if (!IsOpen) { // Initialize if uninitialized. Initialize(); // Make the file path absolute if it is relative. m_fileName = FilePath.GetAbsolutePath(m_fileName); // Create the file directory if it does not exist. if (!Directory.Exists(FilePath.GetDirectoryName(m_fileName))) Directory.CreateDirectory(FilePath.GetDirectoryName(m_fileName)); // Open if file exists, or create it if it doesn't. m_fileData = new FileStream(m_fileName, FileMode.OpenOrCreate, m_fileAccessMode, FileShare.ReadWrite); // Load records into memory if specified to do so. if (m_loadOnOpen) Load(); // Watch for changes to the file content on disk. if ((object)m_fileWatcher != null) m_fileWatcher.EnableRaisingEvents = true; if (m_autoSaveInterval > 0) { // Start timer for saving records loaded in memory automatically. m_autoSaveTimer.Interval = m_autoSaveInterval; m_autoSaveTimer.Start(); } } } /// /// Closes the file. /// public void Close() { if (IsOpen) { // Stop the timer if it is ticking. m_autoSaveTimer.Stop(); // Stop monitoring for changes to the file. if ((object)m_fileWatcher != null) m_fileWatcher.EnableRaisingEvents = false; // Save records back to the file if specified. if (m_saveOnClose) Save(); // Close the file stream used for file I/O. if ((object)m_fileData != null) { lock (m_fileData) { m_fileData.Dispose(); } } m_fileData = null; // Clear the records loaded in memory. if ((object)m_fileRecords != null) { lock (m_fileRecords) { m_fileRecords.Clear(); } } m_fileRecords = null; } } /// /// Loads records from disk into memory. /// public void Load() { if (IsOpen) { // Waits for any pending request to save records before completing. m_saveWaitHandle.WaitOne(); // Waits for any prior request to load records before completing. m_loadWaitHandle.WaitOne(); m_loadWaitHandle.Reset(); try { OnDataLoading(); if ((object)m_fileRecords == null) m_fileRecords = new List(); List records = new List(ReadFromDisk()); lock (m_fileRecords) { m_fileRecords.Clear(); m_fileRecords.InsertRange(0, records); } OnDataLoaded(); } finally { m_loadWaitHandle.Set(); } } else { throw new InvalidOperationException(string.Format("{0} \"{1}\" is not open", this.GetType().Name, m_fileName)); } } /// /// Saves records loaded in memory to disk. /// /// /// is equivalent to when records are not loaded in memory. /// public void Save() { if (IsOpen) { // Waits for any pending request to save records before completing. m_saveWaitHandle.WaitOne(); // Waits for any prior request to load records before completing. m_loadWaitHandle.WaitOne(); m_saveWaitHandle.Reset(); try { OnDataSaving(); // Saves (persists) records to the file, if present in memory. if ((object)m_fileRecords != null) { lock (m_fileRecords) { WriteToDisk(m_fileRecords); } } m_fileData.Flush(); File.SetLastWriteTime(m_fileName, DateTime.Now); OnDataSaved(); } finally { m_saveWaitHandle.Set(); } } else { throw new InvalidOperationException(string.Format("{0} \"{1}\" is not open", this.GetType().Name, m_fileName)); } } /// /// Writes specified records to disk if records were not loaded in memory otherwise updates the records in memory. /// /// Records to be written. /// /// This operation will causes existing records to be deleted and replaced with the ones specified. /// public virtual void Write(IEnumerable records) { if (IsOpen) { if ((object)m_fileRecords == null) { WriteToDisk(records); } else { lock (m_fileRecords) { m_fileRecords.Clear(); m_fileRecords.InsertRange(0, records); } } } else { throw new InvalidOperationException(string.Format("{0} \"{1}\" is not open", this.GetType().Name, m_fileName)); } } /// /// Writes specified record to disk if records were not loaded in memory otherwise updates the record in memory. /// /// 1-based index of the record to be written. /// Record to be written. public virtual void Write(int recordIndex, T record) { if (IsOpen) { if ((object)record != null) { int lastRecordIndex = (object)m_fileRecords == null ? RecordsOnDisk : RecordsInMemory; // Write missing intermediate records. for (int i = lastRecordIndex + 1; i < recordIndex; i++) { Write(i, CreateNewRecord(i)); } if ((object)m_fileRecords == null) { // Write directly to the file. WriteToDisk(recordIndex, record); } else { // Update in-memory record list. lastRecordIndex = RecordsInMemory; if (recordIndex == lastRecordIndex + 1) { // Add new record. lock (m_fileRecords) { m_fileRecords.Add(record); } } else if (recordIndex <= lastRecordIndex) { // Update existing record. lock (m_fileRecords) { m_fileRecords[recordIndex - 1] = record; } } } } else { throw new ArgumentNullException("record"); } } else { throw new InvalidOperationException(string.Format("{0} \"{1}\" is not open", this.GetType().Name, m_fileName)); } } /// /// Reads file records from disk if records were not loaded in memory otherwise returns the records in memory. /// /// Records of the file. public virtual IEnumerable Read() { if (IsOpen) { if ((object)m_fileRecords == null) { // Reads persisted records if no records are in memory. return ReadFromDisk(); } else { // Reads records in memory. lock (m_fileRecords) { return m_fileRecords; } } } else { throw new InvalidOperationException(string.Format("{0} \"{1}\" is not open", this.GetType().Name, m_fileName)); } } /// /// Reads specified file record from disk if records were not loaded in memory otherwise returns the record in memory. /// /// 1-based index of the record to be read. /// Record with the specified ID if it exists; otherwise null. public virtual T Read(int recordIndex) { if (IsOpen) { T record = default(T); if (recordIndex > 0) { // ID of the requested record is valid. if ((object)m_fileRecords == null && recordIndex <= RecordsOnDisk) { // Reads the requested record exists in the file. record = ReadFromDisk(recordIndex); } else if (((object)m_fileRecords != null) && recordIndex <= RecordsInMemory) { // Uses the requested record from memory. lock (m_fileRecords) { record = m_fileRecords[recordIndex - 1]; } } } return record; } else { throw new InvalidOperationException(string.Format("{0} \"{1}\" is not open", this.GetType().Name, m_fileName)); } } /// /// Raises the event. /// protected virtual void OnFileModified() { if ((object)FileModified != null) FileModified(this, EventArgs.Empty); } /// /// Raises the event. /// protected virtual void OnDataLoading() { if ((object)DataLoading != null) DataLoading(this, EventArgs.Empty); } /// /// Raises the event. /// protected virtual void OnDataLoaded() { if ((object)DataLoaded != null) DataLoaded(this, EventArgs.Empty); } /// /// Raises the event. /// protected virtual void OnDataSaving() { if ((object)DataSaving != null) DataSaving(this, EventArgs.Empty); } /// /// Raises the event. /// protected virtual void OnDataSaved() { if ((object)DataSaved != null) DataSaved(this, EventArgs.Empty); } /// /// Releases the unmanaged resources used by the file and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (!m_disposed) { try { // This will be done regardless of whether the object is finalized or disposed. if (disposing) { // This will be done only when the object is disposed by calling Dispose(). Close(); SaveSettings(); if ((object)m_loadWaitHandle != null) m_loadWaitHandle.Close(); if ((object)m_saveWaitHandle != null) m_saveWaitHandle.Close(); if ((object)m_autoSaveTimer != null) { m_autoSaveTimer.Elapsed -= m_autoSaveTimer_Elapsed; m_autoSaveTimer.Dispose(); } if ((object)m_fileWatcher != null) { m_fileWatcher.Changed -= m_fileWatcher_Changed; m_fileWatcher.Dispose(); } } } finally { m_disposed = true; // Prevent duplicate dispose. if (Disposed != null) Disposed(this, EventArgs.Empty); } } } /// /// Re-opens the file if currently open. /// private void ReOpen() { if (IsOpen) { Close(); Open(); } } /// /// Writes records to disk. /// /// Records to be written to disk. private void WriteToDisk(IEnumerable records) { // Write all records to disk. int count = 0; lock (m_fileData) { m_fileData.Seek(0L, SeekOrigin.Begin); foreach (T item in records) { item.CopyBinaryImageToStream(m_fileData); count++; } } // Discard previously existing records that were not written. lock (m_fileData) { m_fileData.SetLength(count * (long)m_recordBuffer.Length); } } /// /// Writes single record to disk. /// /// 1-based index of the record to be written to disk. /// Record to be written to disk. private void WriteToDisk(int recordIndex, T record) { lock (m_fileData) { m_fileData.Seek((recordIndex - 1) * (long)record.BinaryLength, SeekOrigin.Begin); record.CopyBinaryImageToStream(m_fileData); } } /// /// Reads all records from disk. /// /// Records from disk. private IEnumerable ReadFromDisk() { for (int i = 1; i <= RecordsOnDisk; i++) { yield return ReadFromDisk(i); } } /// /// Read single record from disk. /// /// 1-based index of the record to be read. /// Record from the disk. private T ReadFromDisk(int recordIndex) { T newRecord = CreateNewRecord(recordIndex); lock (m_fileData) { m_fileData.Seek((recordIndex - 1) * (long)m_recordBuffer.Length, SeekOrigin.Begin); m_fileData.Read(m_recordBuffer, 0, m_recordBuffer.Length); newRecord.ParseBinaryImage(m_recordBuffer, 0, m_recordBuffer.Length); } return newRecord; } private void m_autoSaveTimer_Elapsed(object sender, ElapsedEventArgs e) { try { // Automatically save records to disk if loaded in memory. if (m_autoSaveTimer.Enabled && ((object)m_fileRecords != null) && IsOpen) Save(); } catch { } } private void m_fileWatcher_Changed(object sender, FileSystemEventArgs e) { try { OnFileModified(); // Reload records if they have been loaded in memory and reloading is enabled. if ((object)m_fileWatcher != null && m_fileWatcher.EnableRaisingEvents && (object)m_fileRecords != null) Load(); } catch { } } #endregion } }