private void DragDropHandler(params object[] arguments) { ObjectNode toNode = (ObjectNode)arguments[0]; ObjectNode fromNode = (ObjectNode)arguments[1]; ObjectNode childNode = (ObjectNode)arguments[2]; // Make sure the user has selected a valid source and destination for the operation. It's illegal to move the node // 1. To the root of the tree // 2. From the root of the tree if (toNode == null || fromNode == null) { return; } // Don't allow for circular references. try { // Lock the tables needed for this operation. Debug.Assert(!ClientMarketData.AreLocksHeld); ClientMarketData.ObjectLock.AcquireReaderLock(CommonTimeout.LockWait); // It's critical that circular references aren't created, either by accident or design. First, find the object // record associated with the destination node. ClientMarketData.ObjectRow parentRow = ClientMarketData.Object.FindByObjectId(toNode.ObjectId); if (parentRow == null) { throw new Exception("This object has been deleted"); } // This is the object that is being dragged. Find the row ClientMarketData.ObjectRow childRow = ClientMarketData.Object.FindByObjectId(childNode.ObjectId); if (childRow == null) { throw new Exception("This object has been deleted"); } if (Shadows.Quasar.Common.Relationship.IsChildObject(childRow, parentRow)) { MessageBox.Show(this.TopLevelControl, "This would create a circular references.", "Quasar Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } } 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 { // Release table locks. if (ClientMarketData.ObjectLock.IsReaderLockHeld) { ClientMarketData.ObjectLock.ReleaseReaderLock(); } Debug.Assert(!ClientMarketData.AreLocksHeld); } // Any commands created below will be constructed in this object and sent to the server for execution. RemoteBatch remoteBatch = new RemoteBatch(); // Change the default model of an account. When the destination is an account group, account or sub account and the // source is a model, a command will be constructed to change the default model. if (toNode.ObjectTypeCode == ObjectType.Account && dragNode.ObjectTypeCode == ObjectType.Model) { try { // Lock the tables needed for this operation. Debug.Assert(!ClientMarketData.AreLocksHeld); ClientMarketData.AccountLock.AcquireReaderLock(CommonTimeout.LockWait); // Find the account used, throw an error if it's been deleted. ClientMarketData.AccountRow accountRow; if ((accountRow = ClientMarketData.Account.FindByAccountId(toNode.ObjectId)) == null) { throw new Exception("This account has been deleted"); } // Construct a command to change the default model associated with the account. RemoteAssembly remoteAssembly = remoteBatch.Assemblies.Add("Service.Core"); RemoteType remoteType = remoteAssembly.Types.Add("Shadows.WebService.Core.Account"); RemoteMethod remoteMethod = remoteType.Methods.Add("Update"); remoteMethod.Parameters.Add("rowVersion", accountRow.RowVersion); remoteMethod.Parameters.Add("accountId", accountRow.AccountId); remoteMethod.Parameters.Add("modelId", dragNode.ObjectId); } 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 { // Release table locks. if (ClientMarketData.AccountLock.IsReaderLockHeld) { ClientMarketData.AccountLock.ReleaseReaderLock(); } Debug.Assert(!ClientMarketData.AreLocksHeld); } } else { // If we made it here, the drag-and-drop is interpreted as a command to move a child from one parent to another. try { // Lock the tables needed for this operation. Debug.Assert(!ClientMarketData.AreLocksHeld); ClientMarketData.ObjectTreeLock.AcquireReaderLock(CommonTimeout.LockWait); // Extract the primary identifiers from the user interface nodes. int fromId = fromNode.ObjectId; int toId = toNode.ObjectId; int childId = dragNode.ObjectId; // Find the object in the data model and make sure it still exists. ClientMarketData.ObjectTreeRow objectTreeRow = ClientMarketData.ObjectTree.FindByParentIdChildId(fromNode.ObjectId, dragNode.ObjectId); if (objectTreeRow == null) { throw new Exception("This relationship has been deleted by someone else."); } // Moving a child object from one parent to another must be accomplished as a transaction. Otherwise, an // orhpan object will be created if the operation fails midway through. RemoteTransaction remoteTransaction = remoteBatch.Transactions.Add(); RemoteAssembly remoteAssembly = remoteBatch.Assemblies.Add("Service.Core"); RemoteType remoteType = remoteAssembly.Types.Add("Shadows.WebService.Core.ObjectTree"); // Construct a command delete the old parent relation. RemoteMethod deleteObjectTree = remoteType.Methods.Add("Delete"); deleteObjectTree.Transaction = remoteTransaction; deleteObjectTree.Parameters.Add("rowVersion", objectTreeRow.RowVersion); deleteObjectTree.Parameters.Add("parentId", fromId); deleteObjectTree.Parameters.Add("childId", childId); // Construct a command insert a new parent relation. RemoteMethod insertObjectTree = remoteType.Methods.Add("Insert"); insertObjectTree.Transaction = remoteTransaction; insertObjectTree.Parameters.Add("parentId", toId); insertObjectTree.Parameters.Add("childId", childId); } 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 { // Release table locks. if (ClientMarketData.ObjectTreeLock.IsReaderLockHeld) { ClientMarketData.ObjectTreeLock.ReleaseReaderLock(); } Debug.Assert(!ClientMarketData.AreLocksHeld); } } // If the command batch was built successfully, then execute it. if (remoteBatch != null) { try { // Call the web server to rename the object on the database. Note that this method must be called when there are // no locks to prevent deadlocking. That is why it appears in it's own 'try/catch' block. ClientMarketData.Execute(remoteBatch); } catch (BatchException batchException) { // Display each error in the batch. foreach (RemoteException remoteException in batchException.Exceptions) { MessageBox.Show(remoteException.Message, "Quasar Error"); } } } }
/// <summary> /// Event handler for changing the name of a label. /// </summary> /// <param name="sender">The window control that generated the 'LabelEdit' event.</param> /// <param name="e">Event Parameters used to control the actions taken by the event handler.</param> private void treeView_AfterLabelEdit(object sender, System.Windows.Forms.NodeLabelEditEventArgs e) { // The TreeView has a bug in it: if you leave the edit mode without typing anything, the returned text of the control // will be an empty string. Since we don't want to bother the server or the user with this nonsense, we'll filter out // the possiblity here. if (e.Label == null) { e.CancelEdit = true; return; } // Extract the object's properties from the node. ObjectNode objectNode = (ObjectNode)e.Node; // This command batch is constructed below and sent to the server for execution. RemoteBatch remoteBatch = new RemoteBatch(); // This will insure that table locks are cleaned up. try { // Lock the table Debug.Assert(!ClientMarketData.AreLocksHeld); ClientMarketData.ObjectLock.AcquireReaderLock(CommonTimeout.LockWait); // Find the object in the data model and make sure it still exists. ClientMarketData.ObjectRow objectRow = ClientMarketData.Object.FindByObjectId(objectNode.ObjectId); if (objectRow == null) { throw new Exception("This object has been deleted."); } // Construct a command to rename the object. RemoteAssembly remoteAssembly = remoteBatch.Assemblies.Add("Service.Core"); RemoteType remoteType = remoteAssembly.Types.Add("Shadows.WebService.Core.Object"); RemoteMethod remoteMethod = remoteType.Methods.Add("Update"); remoteMethod.Parameters.Add("rowVersion", objectRow.RowVersion); remoteMethod.Parameters.Add("objectId", objectRow.ObjectId); remoteMethod.Parameters.Add("name", e.Label); } catch (Exception exception) { // Write the error and stack trace out to the debug listener Debug.WriteLine(String.Format("{0}, {1}", exception.Message, exception.StackTrace)); // Cancel the tree operation if we can't execute the command. The text in the tree control will revert to the // previous value. e.CancelEdit = true; } finally { // Release table locks. if (ClientMarketData.ObjectLock.IsReaderLockHeld) { ClientMarketData.ObjectLock.ReleaseReaderLock(); } Debug.Assert(!ClientMarketData.AreLocksHeld); } // If the command batch was built successfully, then execute it. If any part of it should fail, cancel the edit and // display the server errors. if (remoteBatch != null) { try { // Call the web server to rename the object on the database. Note that this method must be called when there are // no locks to prevent deadlocking. That is why it appears in it's own 'try/catch' block. ClientMarketData.Execute(remoteBatch); } catch (BatchException batchException) { // Undo the editing action. This will restore the name of the object to what it was before the operation. e.CancelEdit = true; // Display each error in the batch. foreach (RemoteException remoteException in batchException.Exceptions) { MessageBox.Show(remoteException.Message, "Quasar Error"); } } } }
/// <summary> /// Creates a structure that can be used to open or operate on a generic object. /// </summary> /// <param name="objectNode">A node in the TreeView that represents an object in the Database.</param> /// <returns>A structure that specifies what object to view, what type of object it is, and any optional /// parameters that might be needed to open the object.</returns> private ObjectArgs CreateObjectArgs(ObjectNode objectNode) { // The returned value represents an object selected from the Folder List. It has all the parameters needed to // open a reader. return(new ObjectArgs(objectNode.ObjectTypeCode, objectNode.ObjectId, objectNode.Name)); }
/// <summary> /// Recursively updates the TreeView control with a new tree structure. /// </summary> /// <param name="targetNodeCollection">The target tree for the changes.</param> /// <param name="sourceNodeCollection">The source tree containing the new structure.</param> private void RecurseRefresh(TreeNodeCollection targetNodeCollection, TreeNodeCollection sourceNodeCollection) { // The main idea here is to run through all the nodes looking for Adds, Deletes and Updates. When we find a // Node that shouldn't be in the structure, it's deleted from the tree. Since the tree is made up of linked // lists, we can't rightly delete the current link in the list. For this reason we need to use the index into // the list for spanning the structure instead of the collection operations. When we find an element that // needs to be removed, we can delete it and the index will get us safely to the next element in the list. The // first loop scans the list already in the TreeView structure. for (int targetIndex = 0; targetIndex < targetNodeCollection.Count; targetIndex++) { // Get a reference to the current node in the target (older) list of nodes. ObjectNode childTargetNode = (ObjectNode)targetNodeCollection[targetIndex]; // If we don't find the target (older) element in the source (newer) list, it will be deleted. bool found = false; // Cycle through all of the source (newer) elements looking for changes and removing any elements that // exist in both lists. for (int sourceIndex = 0; sourceIndex < sourceNodeCollection.Count; sourceIndex++) { // Get a reference to the current node in the source (newer) list of nodes. ObjectNode childSourceNode = (ObjectNode)sourceNodeCollection[sourceIndex]; // If the elements are equal (as defined by the key element), then recurse into the structure looking // for changes to the children. After that, check the Node for any changes since it was added to the // tree. if (childTargetNode.ObjectId == childSourceNode.ObjectId) { // Recurse down into the tree structures bringing all the children in sync with the new structure. RecurseRefresh(childTargetNode.Nodes, childSourceNode.Nodes); // Check the Nodes Name. Update it if there's a change. if (childTargetNode.Text != childSourceNode.Name) { childTargetNode.Text = childSourceNode.Name; } // At this point, we've checked all the children and applied any changes to the node. Remove it // from the list. Any elements left in the source list are assumed to be new members and will be // added to the tree structure. That's why it's important to remove the ones already in the tree. sourceNodeCollection.Remove(childSourceNode); sourceIndex--; // This will signal the target loop that this element still exists in the structure. If it isn't // found, it'll be deleted. found = true; } } // If the target (older) element isn't found in the source (newer) tree, it's deleted. if (!found) { targetNodeCollection.Remove(childTargetNode); targetIndex--; } } // Any element that doesn't already exist in the target (older) tree, is copied from the source (newer) tree, // along with all it's children. for (int nodeIndex = 0; nodeIndex < sourceNodeCollection.Count; nodeIndex++) { ObjectNode objectNode = (ObjectNode)sourceNodeCollection[nodeIndex--]; sourceNodeCollection.Remove(objectNode); targetNodeCollection.Add(objectNode); } }
/// <summary> /// Populates the Tree Control with data collected in the initialization thread. /// </summary> /// <param name="objectNode">Contains a hierarchical organization of objects.</param> private void RefreshTree(ObjectNode objectNode) { // This will recursively reconstruct the tree from the relational data gathered from the background thread. RecurseRefresh(this.treeView.Nodes, objectNode.Nodes); }
/// <summary> /// This procedure is used to update the TreeView control from the background. /// </summary> private void FolderListThread(params object[] argument) { // This node acts as the root that we'll use to build the tree folder list tree. It will eventually be // passed back to the main thread and into the TreeView control. ObjectNode rootNode = new ObjectNode(); try { // Lock all the tables that we'll reference while building a blotter document. Debug.Assert(!ClientMarketData.AreLocksHeld); ClientMarketData.FolderLock.AcquireReaderLock(CommonTimeout.LockWait); ClientMarketData.LoginLock.AcquireReaderLock(CommonTimeout.LockWait); ClientMarketData.ObjectLock.AcquireReaderLock(CommonTimeout.LockWait); ClientMarketData.ObjectTreeLock.AcquireReaderLock(CommonTimeout.LockWait); // Recursively build the Tree View from the user's folder. ClientMarketData.LoginRow loginRow = ClientMarketData.Login.FindByLoginId(ClientPreferences.LoginId); if (loginRow != null && !loginRow.IsFolderIdNull()) { ClientMarketData.FolderRow folderRow = ClientMarketData.Folder.FindByFolderId(loginRow.FolderId); rootNode.Nodes.Add(new ObjectNode(folderRow.ObjectRow)); } } catch (Exception e) { // Catch the most general error and send it to the debug console. Debug.WriteLine(e.Message); } finally { // Release the tables used to build the folder list. if (ClientMarketData.FolderLock.IsReaderLockHeld) { ClientMarketData.FolderLock.ReleaseReaderLock(); } if (ClientMarketData.LoginLock.IsReaderLockHeld) { ClientMarketData.LoginLock.ReleaseReaderLock(); } if (ClientMarketData.ObjectLock.IsReaderLockHeld) { ClientMarketData.ObjectLock.ReleaseReaderLock(); } if (ClientMarketData.ObjectTreeLock.IsReaderLockHeld) { ClientMarketData.ObjectTreeLock.ReleaseReaderLock(); } Debug.Assert(!ClientMarketData.AreLocksHeld); } // Wait for the window handle to be created before trying to write to the control. The thread only needs to wait once. // After that, this event is permanently signaled. this.handleCreatedEvent.WaitOne(); // At this point, the tree is complete. This background thread needs to wait for a signal from the main application // thread that the TreeView control has been created. Once we get the signal, pass the entire tree structure back to // the main thread so it can copy the tree members. Note that the handle needs to be checked before invoking the // foreground method, even though we waited above for the "Handle Create" event. This test is meant to prevent events // from being called when the application is shutting down. if (this.IsHandleCreated) { Invoke(objectNodeDelegate, new object[] { rootNode }); } }