/// <summary> /// Event trigger when any record in the data model has changed. /// </summary> /// <param name="sender">Object originating the event.</param> public static void OnEndMerge(object sender) { // If a handler has been associated with this event, invoke it. if (ClientMarketData.EndMerge != null) { ClientMarketData.EndMerge(sender, EventArgs.Empty); } }
/// <summary> /// Execute a command batch and synchronizes the client data model with the newer records from the server. /// </summary> public static void Execute(RemoteBatch remoteBatch) { // This 'try' block will insure that the mutual exclusion locks are released. try { // Maure sure only one thread at a time tries to refresh the data model. ClientMarketData.refreshMutex.WaitOne(); // IMPORTANT CONCEPT: The rowVersion keeps track of the state of the client in-memory database. The server returns // a value for us to use on the next cycle. If, for any reason, the merge of the data on the client should fail, // we won't use the new value on the next cycle. That is, we're going to keep on asking for the same incremental // set of records until we've successfully merged them. This guarantees that the client and server databases stay // consistent with each other. DateTime timeStamp = ClientMarketData.timeStamp; long rowVersion = ClientMarketData.rowVersion; // IMPORTANT CONCEPT: Executing the 'Reconcile' Web Service with a rowVersion returns to the client a DataSet with // only records that are the same age or younger than the rowVersion. This reduces the traffic on the network to // include only the essential records. This set also contains the deleted records. When the DataSet that is // returned from this Web Service is merged with the current data, the client will be reconciled with the server as // of the 'rowVersion' returned from the server. DataSet resultSet; WebClient webClient = new WebClient(); DataSet dataSet = webClient.ExecuteAndReconcile(ref timeStamp, ref rowVersion, remoteBatch, out resultSet); remoteBatch.Merge(resultSet); // If the time stamps are out of sync, then the server has reset since our last refresh (or this is the first time // refreshing the data mdoel). This will reset the client side of the data model so that, after this refresh, the // client and the server will be in sync again. if (ClientMarketData.timeStamp != timeStamp) { ClientMarketData.rowVersion = 0; ClientMarketData.timeStamp = timeStamp; ClientMarketData.Clear(); } // Optimization: Don't merge the results if there's nothing to merge. if (rowVersion > ClientMarketData.rowVersion) { // IMPORTANT CONCEPT: This broadcast can be used to set up conditions for the data event handlers. Often, // optimizing algorithms will be used to consolidate the results of a merge. This will allow the event driven // logic to clear previous results and set up initial states for handling the bulk update of data. ClientMarketData.OnBeginMerge(typeof(ClientMarketData)); // This 'try' block will insure that the locks are released if there's an error. try { // IMPORTANT CONCEPT: Make sure that there aren't any nested locks. Table locks have to be aquired in the // same order for every thread that uses the shared data model. It's possible that the current thread may // have locked table 'B' if the preamble, then try to lock table 'A' during the processing of a command // batch. If another thread aquires the lock on table 'A' and blocks on table 'B', we will have a // deadlock. This is designed to catch situations where the 'Execute' method is called while tables are // already locked. Debug.Assert(!ClientMarketData.AreLocksHeld); // IMPORTANT CONCEPT: Since the results will still be on the server if the client misses a refresh cycle, // we take the attitude that this update process doesn't have to wait for locks. That is, if we can't get // all the tables locked quickly, we'll just wait until the next refresh period to get the results. This // effectively prevents deadlocking on the client. Make sure all the tables are locked before populating // them. foreach (TableLock tableLock in ClientMarketData.TableLocks) { tableLock.AcquireWriterLock(ClientTimeout.LockWait); } // IMPORANT CONCEPT: Once all the write locks have been obtained, we can merge the results. This will // trigger the events associated with the tables for updated and deleted rows. Also, if // "AcceptChanges" is invoked for a DataSet or a Table, every single record in the DataSet or DataTable // will be "Committed", even though they are unchanged. This is a VERY inefficient operation. To get // around this, an ArrayList of modified records is constructed during the Merge operation. After the // merge, only the new records are committed. ClientMarketData.mergedRows.Clear(); ClientMarketData.Merge(dataSet); for (int index = 0; index < ClientMarketData.mergedRows.Count; index++) { ((DataRow)ClientMarketData.mergedRows[index]).AcceptChanges(); } // If the merge operation was successful, then we can use the new rowVersion for the next cycle. Any // exception before this point will result in a request of the same set of data becaue the rowVersion was // never updated. ClientMarketData.rowVersion = rowVersion; } catch (ConstraintException) { // Write out the exact location of the error. foreach (DataTable dataTable in ClientMarketData.Tables) { foreach (DataRow dataRow in dataTable.Rows) { if (dataRow.HasErrors) { Console.WriteLine("Error in '{0}': {1}", dataRow.Table.TableName, dataRow.RowError); } } } } catch (Exception exception) { // Write the error and stack trace out to the debug listener Debug.WriteLine(String.Format("{0}, {1}", exception.Message, exception.StackTrace)); } finally { // No matter what happens above, we need to release the locks acquired above. foreach (TableLock tableLock in ClientMarketData.TableLocks) { if (tableLock.IsWriterLockHeld) { tableLock.ReleaseWriterLock(); } } } // IMPORTANT CONCEPT: When the merge is complete and the tables are unlocked, this will broadcast an event // which allows optimization code to consolidate the results, examine the changed values and update reports // based on the changed data. ClientMarketData.OnEndMerge(typeof(ClientMarketData)); } } finally { // Other threads can now request a refresh of the data model. ClientMarketData.refreshMutex.ReleaseMutex(); } // Throw a specialized exception if the server returned any errors in the RemoteBatch structure. if (remoteBatch.HasExceptions) { throw new BatchException(remoteBatch); } }
/// <summary> /// ThreadBackground /// </summary> /// <remarks> /// This procedure runs as a thread. It will asynchronously query the database for the raw data that makes up the /// hierarchy of objects found in the application. It will then organize the raw, flat data into a hierarchial /// tree that's suitable to be viewed in a TreeView control.</remarks> private static void ThreadBackground() { // Create a web client for communicating with the server. WebClient webClient = new WebClient(); // This thread body is executed until the application is terminated. while (true) { try { // Maure sure only one thread at a time tries to refresh the data model. ClientMarketData.refreshMutex.WaitOne(); // IMPORTANT CONCEPT: The rowVersion keeps track of the state of the client in-memory database. The server // returns a value for us to use on the next cycle. If, for any reason, the merge of the data on the client // should fail, we won't use the new value on the next cycle. That is, we're going to keep on asking for the // same incremental set of records until we've successfully merged them. This guarantees that the client and // server databases stay consistent with each other. DateTime timeStamp = ClientMarketData.timeStamp; long rowVersion = ClientMarketData.rowVersion; // IMPORTANT CONCEPT: Executing the 'GetClientMarketData' with a rowVersion returns to the client a DataSet // with only records that are the same age or younger than the rowVersion. This reduces the traffic on the // network to include only the essential records. We are also merging it with the current ClientMarketData, // which adds new records and records that were deleted by the server. DataSet dataSet = webClient.Reconcile(ref timeStamp, ref rowVersion); // If the time stamps are out of sync, then the server has reset since our last refresh (or this is the first // time refreshing the market data). This will reset the client side of the data model so that -- after the // current results are merged -- the client and the server will be in sync again. if (ClientMarketData.timeStamp != timeStamp) { ClientMarketData.rowVersion = 0; ClientMarketData.timeStamp = timeStamp; ClientMarketData.Clear(); } // Optimization: Don't merge the results if there's nothing to merge. if (rowVersion > ClientMarketData.rowVersion) { // IMPORTANT CONCEPT: This broadcast can be used to set up conditions for the data event handlers. Often, // optimizing algorithms will be used to consolidate the results of a merge. This will allow the event // driven logic to clear previous results and set up initial states for handling the bulk update of data. ClientMarketData.OnBeginMerge(typeof(ClientMarketData)); try { // IMPORTANT CONCEPT: Since the results will still be on the server if the client misses a refresh // cycle, we take the attitude that this update process doesn't have to wait for locks. That is, if we // can't get all the tables locked quickly, we'll just wait until the next refresh period to get the // results. This effectively prevents deadlocking on the client. Make sure all the tables are locked // before populating them. foreach (TableLock tableLock in ClientMarketData.TableLocks) { tableLock.AcquireWriterLock(ClientTimeout.LockWait); } // IMPORANT CONCEPT: Once all the write locks have been obtained, we can merge the results. This will // trigger the events associated with the tables for updated and deleted rows. Also, if // "AcceptChanges" is invoked for a DataSet or a Table, every single record in the DataSet or DataTable // will be "Committed", even though they are unchanged. This is a VERY inefficient operation. To get // around this, an ArrayList of modified records is constructed during the Merge operation. After the // merge, only the new records are committed. ClientMarketData.mergedRows.Clear(); ClientMarketData.Merge(dataSet); for (int index = 0; index < ClientMarketData.mergedRows.Count; index++) { ((DataRow)ClientMarketData.mergedRows[index]).AcceptChanges(); } // If the merge operation was successful, then we can use the new rowVersion for the next cycle. Any // exception before this point will result in a request of the same set of data becaue the rowVersion // was never updated. ClientMarketData.rowVersion = rowVersion; } catch (ApplicationException applicationException) { // Tyipcally, a failure to gain a lock will invoke this exception. If this information ends up being // too much for the log device, we can alway inhibit exception catching here. Debug.WriteLine(applicationException.Message); } catch (ConstraintException) { // Write out the exact location of the error. foreach (DataTable dataTable in ClientMarketData.Tables) { foreach (DataRow dataRow in dataTable.Rows) { if (dataRow.HasErrors) { Console.WriteLine("Error in '{0}': {1}", dataRow.Table.TableName, dataRow.RowError); } } } } catch (Exception exception) { // Write the error and stack trace out to the debug listener Debug.WriteLine(String.Format("{0}, {1}", exception.Message, exception.StackTrace)); } finally { // No matter what happens above, we need to release the locks acquired above. foreach (TableLock tableLock in ClientMarketData.TableLocks) { if (tableLock.IsWriterLockHeld) { tableLock.ReleaseWriterLock(); } } } // IMPORTANT CONCEPT: When the merge is complete, this will broadcast an event which allows optimization code // to consolidate the results, examine the changed values and update reports based on the changed data. ClientMarketData.OnEndMerge(typeof(ClientMarketData)); } } catch (Exception exception) { // Write the error and stack trace out to the debug listener Debug.WriteLine(String.Format("{0}, {1}", exception.Message, exception.StackTrace)); } finally { // Other threads can now request a refresh of the data model. ClientMarketData.refreshMutex.ReleaseMutex(); } // Wait for a predetermined interval before refreshing the data model again. Thread.Sleep(ClientTimeout.RefreshInterval); } }
/// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { this.components = new System.ComponentModel.Container(); System.Resources.ResourceManager resources = new System.Resources.ResourceManager(typeof(FolderList)); this.treeView = new System.Windows.Forms.TreeView(); this.contextMenu = new System.Windows.Forms.ContextMenu(); this.menuItemOpen = new System.Windows.Forms.MenuItem(); this.menuItem3 = new System.Windows.Forms.MenuItem(); this.menuItemDelete = new System.Windows.Forms.MenuItem(); this.menuItemRename = new System.Windows.Forms.MenuItem(); this.menuItem1 = new System.Windows.Forms.MenuItem(); this.menuItemProperties = new System.Windows.Forms.MenuItem(); this.imageList = new System.Windows.Forms.ImageList(this.components); this.clientClientMarketData = new Shadows.Quasar.Client.ClientMarketData(this.components); this.SuspendLayout(); // // treeView // this.treeView.AllowDrop = true; this.treeView.ContextMenu = this.contextMenu; this.treeView.Dock = System.Windows.Forms.DockStyle.Fill; this.treeView.ImageList = this.imageList; this.treeView.LabelEdit = true; this.treeView.Name = "treeView"; this.treeView.Size = new System.Drawing.Size(150, 112); this.treeView.Sorted = true; this.treeView.TabIndex = 0; this.treeView.MouseDown += new System.Windows.Forms.MouseEventHandler(this.treeView_MouseDown); this.treeView.DragOver += new System.Windows.Forms.DragEventHandler(this.treeView_DragOver); this.treeView.AfterSelect += new System.Windows.Forms.TreeViewEventHandler(this.treeView_AfterSelect); this.treeView.AfterLabelEdit += new System.Windows.Forms.NodeLabelEditEventHandler(this.treeView_AfterLabelEdit); this.treeView.ItemDrag += new System.Windows.Forms.ItemDragEventHandler(this.treeView_ItemDrag); this.treeView.DragDrop += new System.Windows.Forms.DragEventHandler(this.treeView_DragDrop); // // contextMenu // this.contextMenu.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { this.menuItemOpen, this.menuItem3, this.menuItemDelete, this.menuItemRename, this.menuItem1, this.menuItemProperties }); // // menuItemOpen // this.menuItemOpen.Index = 0; this.menuItemOpen.Text = "&Open"; this.menuItemOpen.Click += new System.EventHandler(this.menuItemOpen_Click); // // menuItem3 // this.menuItem3.Index = 1; this.menuItem3.Text = "-"; // // menuItemDelete // this.menuItemDelete.Index = 2; this.menuItemDelete.Text = "&Delete"; // // menuItemRename // this.menuItemRename.Index = 3; this.menuItemRename.Text = "&Rename"; this.menuItemRename.Click += new System.EventHandler(this.menuItemRename_Click); // // menuItem1 // this.menuItem1.Index = 4; this.menuItem1.Text = "-"; // // menuItemProperties // this.menuItemProperties.Index = 5; this.menuItemProperties.Text = "&Properties"; this.menuItemProperties.Click += new System.EventHandler(this.menuItemProperties_Click); // // imageList // this.imageList.ColorDepth = System.Windows.Forms.ColorDepth.Depth24Bit; this.imageList.ImageSize = new System.Drawing.Size(16, 16); this.imageList.ImageStream = ((System.Windows.Forms.ImageListStreamer)(resources.GetObject("imageList.ImageStream"))); this.imageList.TransparentColor = System.Drawing.Color.Aqua; // // FolderList // this.Controls.AddRange(new System.Windows.Forms.Control[] { this.treeView }); this.Name = "FolderList"; this.Size = new System.Drawing.Size(150, 112); this.ResumeLayout(false); }