/// <summary> /// Occurs when the element is laid out, rendered, and ready for interaction. /// </summary> /// <param name="sender">The object where the event handler is attached.</param> /// <param name="routedEventArgs">The event data.</param> protected virtual void OnLoaded(Object sender, RoutedEventArgs routedEventArgs) { // This provides a data context for the blotter. AssetNetworkItem assetNetworkItem = this.DataContext as AssetNetworkItem; this.blotterIdField = assetNetworkItem.EntityId; // Data model changes can come in high frequency bursts. To filter out the events that are relevant for this blotter view, we're going to flatten out // the hierarchy and put all the blotter identifiers in a set. Then, when a given working order row (or any row related to a working order) is // modified or deleted, we can use this set as a filter to quickly establish what events need attension and what events can be ignored. this.blotterIdSet.Clear(); this.DiscoverBlotters(DataModel.Blotter.BlotterKey.Find(this.BlotterId)); // Call the virtual version of this method to initialize the event handlers. Descendants can override this behavior to add additional tables to watch // for additional metadata. It should be noted that the BlotterId and the expanded list of blotters has been generated and is available to the // descendant classes when this virtual method is called. this.OnLoaded(); // The visual elements that that display metadata key/value pairs are initialized here. this.metadataField.Clear(); this.InitializeMetadata(); // This will calculate the initial values for the displayed metadata. After this initial pass, in order to prevent scanning all the working orders in // all the blotters for every change that comes in, we'll peform delta operations on just the properties that have changed. this.CalculateMetadata(); // This will send a message up to the frame that there is a collection of metadata available. This will be bound to the DetailBar by the frame and the // items will appear hosted in the Detail bar. These items in the DetailBar are not copies but the elements created above. Any change to the items // above will be reflected immediately in the DetailBar. this.RaiseEvent(new ItemsSourceEventArgs(ExplorerFrame.DetailBarChangedEvent, this.metadataField)); }
/// <summary> /// Invoked when an unhandled DragDrop.DragEnter attached event reaches an element in its route that is derived from this class. /// </summary> /// <param name="dragEventArgs">The DragEventArgs that contains the event data.</param> protected override void OnDrop(DragEventArgs dragEventArgs) { // Close (and dereference) the window used to display the object being dragged. this.dragWindow.Close(); this.dragWindow = null; // Attempt to find the element currently selected in the page. This item will be the parent of any object dropped onto the surface. AssetNetworkItem parentItem = ExplorerHelper.FindExplorerItem(this.DataContext as AssetNetworkItem, this.Source) as AssetNetworkItem; if (parentItem != null) { // We can import files dropped onto the surface. If the format specifies one or more files dragged from the Windows Explorer, then set call a // handler to import each filed based on the file type (that is, the extension). String[] paths = dragEventArgs.Data.GetData("FileDrop") as String[]; if (paths != null) { foreach (String path in paths) { DocumentProperty externalObject; if (DirectoryPage.documentProperties.TryGetValue(Path.GetExtension(path).ToLower(), out externalObject)) { DirectoryPage.CreateDocument(parentItem, path, externalObject); } } } } }
/// <summary> /// Handles a change to the interpreted Source URI for this frame. /// </summary> /// <param name="oldUri">The old Source URI.</param> /// <param name="newUri">The new Source URI.</param> protected override void OnSourceChanged(Uri oldUri, Uri newUri) { // The main idea here is that we have a data model that streams in. There is no 'start' or 'end' of load, so we need to deal with the model as it // builds itself. Because the AssetNetworkCollection can build itself incrementally as data loads, we can get into a scenario where the outline of the // list is built and all the resources are loaded, but we haven't yet loaded the 'Property Store' table, where the metadata containing the viewer for // any given object is kept. This logic here will hook itself into the current AssetNetworkItem selected and watch for an update to the viewer. If // and when a new viewer should arrive, we'll just poke it in. This will unhook the previous AssetNetworkItem from watching for the change. if (oldUri != null) { AssetNetworkItem oldItem = ExplorerHelper.FindExplorerItem(this.DataContext as IExplorerItem, oldUri) as AssetNetworkItem; if (oldItem != null) { oldItem.PropertyChanged -= this.OnItemPropertyChanged; } } // This will allow us to poke a new viewer in should the properties for the current AssetNetworkItem selected be changed by an update from the data // model. if (newUri != null) { AssetNetworkItem newItem = ExplorerHelper.FindExplorerItem(this.DataContext as IExplorerItem, newUri) as AssetNetworkItem; if (newItem != null) { newItem.PropertyChanged += this.OnItemPropertyChanged; } } }
/// <summary> /// Represents the method that will handle the PropertyChanged event raised when a property is changed on a component. /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">A PropertyChangedEventArgs that contains the event data.</param> void OnItemPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { // In this scenario, we may have constructed an AssetNetworkCollection hierarchy for the application, but not have all the properties loaded, such as // the viewer for a given object. When the viewer finally does arrive, or if the viewer is changed dynamically, this will force the new viewer to be the // actual URI source of the frame. This shouldn't be confused with the intepreted source, which is really just a method of Source URI mapping. If I // hadn't been lazy, I might have just tried to use the SourceUri Mapping data structures on this window to map an '/Administrator' into a // 'pack://application:,,,/Teraque.AssetNetwork.Navigator;component/DirectoryPage.xaml?Administrator' source. AssetNetworkItem assetNetworkItem = sender as AssetNetworkItem; if (e.PropertyName == "Viewer") { this.ActualSource = assetNetworkItem.Viewer; } }
/// <summary> /// Occurs when the element is laid out, rendered, and ready for interaction. /// </summary> /// <param name="sender">The object where the event handler is attached.</param> /// <param name="routedEventArgs">The event data.</param> void OnLoaded(Object sender, RoutedEventArgs routedEventArgs) { // This provides a data context for the blotter. AssetNetworkItem assetNetworkItem = this.DataContext as AssetNetworkItem; this.entityIdField = assetNetworkItem.EntityId; // Link the page into the data model. DataModel.Entity.EntityRowChanged += this.OnEntityRowChanged; // This will calculate the initial values for the displayed metadata. this.CalculateMetadata(); // This will send a message up to the frame that there is a collection of metadata available. This will be bound to the DetailBar by the frame and the // items will appear hosted in the Detail bar. These items in the DetailBar are not copies but the elements created above. Any change to the items // above will be reflected immediately in the DetailBar. this.RaiseEvent(new ItemsSourceEventArgs(ExplorerFrame.DetailBarChangedEvent, this.metadata)); }
/// <summary> /// Copies the information from the data model into the item. /// </summary> /// <param name="owner">The owner collection.</param> /// <param name="entityRow">The record in the data model that contains the information for this item.</param> protected void Copy(AssetNetworkCollection owner, DataModel.EntityRow entityRow) { // Validate the parameters if (owner == null) { throw new ArgumentNullException("owner"); } if (entityRow == null) { throw new ArgumentNullException("entityRow"); } // Copy the basic information out of the data model. These statements will also cause the NotifyPropertyChange event to fire. this.DateCreated = entityRow.CreatedTime; this.DateModified = entityRow.ModifiedTime; this.EntityId = entityRow.EntityId; this.Name = entityRow.Name; this.TypeDescription = entityRow.TypeRow.Description; this.TypeId = entityRow.TypeId; // Create properties from the metadata associated with this entity. foreach (DataModel.PropertyStoreRow propertyStoreRow in entityRow.GetPropertyStoreRows()) { // Copy the viewer property. if (propertyStoreRow.PropertyId == PropertyId.Viewer) { this.Viewer = new Uri(Encoding.Unicode.GetString(propertyStoreRow.Value as Byte[]), UriKind.RelativeOrAbsolute); } // Copy the data property. if (propertyStoreRow.PropertyId == PropertyId.Data) { this.Data = propertyStoreRow.Value as Byte[]; } } // This will disassemble the icon and give us the basic image sizes supported by the application framework. Dictionary <ImageSize, ImageSource> images = ImageHelper.DecodeIcon(Convert.FromBase64String(entityRow.ImageRow.Image)); this.SmallImageSource = images[ImageSize.Small]; this.MediumImageSource = images[ImageSize.Medium]; this.LargeImageSource = images[ImageSize.Large]; this.ExtraLargeImageSource = images[ImageSize.ExtraLarge]; // The next part of the copy operation involves recursing into the children and copying, moving or removing the items to reflect the current hierarchy. // This query is the meat of recursing into the hierarchy. This will create a sorted array of all the child items of this node. var children = from entityTreeItem in entityRow.GetEntityTreeRowsByFK_Entity_EntityTree_ParentId() where entityTreeItem.EntityRowByFK_Entity_EntityTree_ChildId.IsContainer == true orderby entityTreeItem.EntityRowByFK_Entity_EntityTree_ChildId.Name select entityTreeItem.EntityRowByFK_Entity_EntityTree_ChildId; DataModel.EntityRow[] childArray = children.ToArray <DataModel.EntityRow>(); // This list is part of a MVVM. Since this collection is designed to be bound to the user interface, it is important not to wipe it clean and rebuild // it each time something changes. The main idea of this algorithm is to find out what children are new, what children need to be updated and what // children need to be deleted without disturbing the other children. Int32 sourceIndex = 0; Int32 targetIndex = 0; while (targetIndex < this.Children.Count && sourceIndex < childArray.Length) { // The names of the node (within the scope of their parent node) is unique, just like a file system. If the list of new children doesn't match up // with the list provided by the data model, then we will insert, update or delete the list in order to reconcile the differences. AssetNetworkItem targetItem = this.Children[targetIndex] as AssetNetworkItem; DataModel.EntityRow childRow = childArray[sourceIndex]; // Items no longer in the data model are deleted from the list. Items that are already in the list have their contents copied (including children) // and new items are created (including their children) and added to the list. switch (String.Compare(targetItem.Name, childRow.Name, true, CultureInfo.CurrentCulture)) { case -1: // Remove children no longer in the data model. AssetNetworkItem removedItem = this.Children[targetIndex] as AssetNetworkItem; owner.RemoveDescendant(removedItem); this.Children.Remove(removedItem); break; case 0: // Copy items that are already in the list. targetItem.Copy(owner, childRow); sourceIndex++; targetIndex++; break; case 1: // Add new items (and their children) when they aren't in the list. AssetNetworkItem addedItem = new AssetNetworkItem(owner, childRow); owner.AddDescendant(addedItem); this.Children.Insert(targetIndex, addedItem); targetIndex++; sourceIndex++; break; } } // This covers the case where there were several children added to the list after the last item in the existing list. In this situation there is // nothing left to reconcile, just a bunch of new items to be concatenated to the current list of children. while (sourceIndex < childArray.Length) { AssetNetworkItem assetNetworkItem = new AssetNetworkItem(owner, childArray[sourceIndex]); owner.AddDescendant(assetNetworkItem); this.Children.Add(assetNetworkItem); sourceIndex++; } // The next part of the copy operation involves recursing into the children and copying, moving or removing the items to reflect the current hierarchy. // This query is the meat of recursing into the hierarchy. This will create a sorted array of all the child items of this node. var leaves = from entityTreeItem in entityRow.GetEntityTreeRowsByFK_Entity_EntityTree_ParentId() where entityTreeItem.EntityRowByFK_Entity_EntityTree_ChildId.IsContainer == false orderby entityTreeItem.EntityRowByFK_Entity_EntityTree_ChildId.Name select entityTreeItem.EntityRowByFK_Entity_EntityTree_ChildId; DataModel.EntityRow[] leavesArray = leaves.ToArray <DataModel.EntityRow>(); // This list is part of a MVVM. Since this collection is designed to be bound to the user interface, it is important not to wipe it clean and rebuild // it each time something changes. The main idea of this algorithm is to find out what leaves are new, what leaves need to be updated and what // leaves need to be deleted without disturbing the other leaves. Int32 sourceLeavesIndex = 0; Int32 targetLeavesIndex = 0; while (targetLeavesIndex < this.Leaves.Count && sourceLeavesIndex < leavesArray.Length) { // The names of the node (within the scope of their parent node) is unique, just like a file system. If the list of new leaves doesn't match up // with the list provided by the data model, then we will insert, update or delete the list in order to reconcile the differences. AssetNetworkItem targetItem = this.Leaves[targetLeavesIndex] as AssetNetworkItem; DataModel.EntityRow leafRow = leavesArray[sourceLeavesIndex]; // Items no longer in the data model are deleted from the list. Items that are already in the list have their contents copied (including leaves) // and new items are created (including their leaves) and added to the list. switch (String.Compare(targetItem.Name, leafRow.Name, true, CultureInfo.CurrentCulture)) { case -1: // Remove leaves no longer in the data model. AssetNetworkItem removedItem = this.Leaves[targetLeavesIndex] as AssetNetworkItem; owner.RemoveDescendant(removedItem); this.Leaves.Remove(removedItem); break; case 0: // Copy items that are already in the list. targetItem.Copy(owner, leafRow); sourceLeavesIndex++; targetLeavesIndex++; break; case 1: // Add new items (and their leaves) when they aren't in the list. AssetNetworkItem addedItem = new AssetNetworkItem(owner, leafRow); owner.AddDescendant(addedItem); this.Leaves.Insert(targetLeavesIndex, addedItem); targetLeavesIndex++; sourceLeavesIndex++; break; } } // This covers the case where there were several leaves added to the list after the last item in the existing list. In this situation there is // nothing left to reconcile, just a bunch of new items to be concatenated to the current list of leaves. while (sourceLeavesIndex < leavesArray.Length) { AssetNetworkItem assetNetworkItem = new AssetNetworkItem(owner, leavesArray[sourceLeavesIndex]); owner.AddDescendant(assetNetworkItem); this.Leaves.Add(assetNetworkItem); sourceLeavesIndex++; } }
/// <summary> /// Creates a data model entry for the given path element. /// </summary> /// <param name="parentItem"></param> /// <param name="path">The full file name of the document to be loaded.</param> /// <param name="externalDocument">The properties used to build the document.</param> static void CreateDocument(AssetNetworkItem parentItem, String path, DocumentProperty externalDocument) { // The general idea here is to create the data structure that will be passed to the server to create the documents. Once built, the same data is used // to modify the client side so it will look like the file was imported instantaneously. If the operation to add the document fails, then we will undo // the transaction but most of the time it will just look zippin' quick. This is list of metadata properties used to create the document. List <PropertyStoreInfo> propertyStoreList = new List <PropertyStoreInfo>(); // Each document requires an image so we can display an icon on the directory page. The icon used for the documents is acquired from the type // information (each type is associated with an icon). DataModel.TypeRow typeRow = DataModel.Type.TypeKey.Find(externalDocument.typeId); // IMPORTANT CONCEPT: In order to create the document on the client side, we need to have unique identifiers. These unique identifiers will be passed // to the server when it is time to create the document and all the records that go with it (metadata, entity tree, etc.). If the operation is not // successful, then we roll back this transaction and all these records will go away, but if the operation is successful, then we'll get back the // actual database record which will, by virtue of having the same identifiers created here, update these records with the actual server information // (most importantly, we'll get back a RowVersion and all the other server-side defaults). CreateDocumentInfo createDocumentInfo = new CreateDocumentInfo(); createDocumentInfo.EntityId = Guid.NewGuid(); createDocumentInfo.EntityTreeId = Guid.NewGuid(); createDocumentInfo.Name = Path.GetFileName(path); createDocumentInfo.ParentEntityId = parentItem.EntityId; createDocumentInfo.TypeId = typeRow.TypeId; // If a viewer has been specified for this file type, then create a metadata property for the viewer. if (!String.IsNullOrEmpty(externalDocument.ViewerUri)) { PropertyStoreInfo viewerPropertyInfo = new PropertyStoreInfo(); viewerPropertyInfo.PropertyStoreId = Guid.NewGuid(); viewerPropertyInfo.PropertyId = PropertyId.Viewer; viewerPropertyInfo.Value = Encoding.Unicode.GetBytes(externalDocument.ViewerUri); propertyStoreList.Add(viewerPropertyInfo); } // This will create a metadata property for the data that goes with an item. In this case, the data is the file contents. PropertyStoreInfo dataPropertyInfo = new PropertyStoreInfo(); dataPropertyInfo.PropertyId = PropertyId.Data; dataPropertyInfo.PropertyStoreId = Guid.NewGuid(); using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read)) { MemoryStream memoryStream = new MemoryStream(); DirectoryPage.Copy(fileStream, memoryStream); Byte[] buffer = new Byte[memoryStream.Length]; Array.Copy(memoryStream.GetBuffer(), buffer, memoryStream.Length); dataPropertyInfo.Value = buffer; } propertyStoreList.Add(dataPropertyInfo); // Package the properties up in an array. createDocumentInfo.Properties = propertyStoreList.ToArray(); // At this point, we have enough data to create the document on the server. We could quit now and just send the message off to the server and wait for // the server to update the client with the newly created document. However, this involves a lag time which can be noticable on public systems. To // make it look like the operation was instantaneous, we're going to create the document on the client side. If the server accepts the web service // call, then all this data will be overwritten by the actual server version. If the operation fails, all this data will be rolled back. DataModel.EntityRow entityRow = DataModel.Entity.NewRow() as DataModel.EntityRow; entityRow.EntityId = createDocumentInfo.EntityId; entityRow.CreatedTime = DateTime.Now; entityRow.ImageId = typeRow.ImageId; entityRow.Name = createDocumentInfo.Name; entityRow.ModifiedTime = DateTime.Now; entityRow.RowVersion = 0; entityRow.IsContainer = false; entityRow.TypeId = createDocumentInfo.TypeId; DataModel.Entity.Rows.Add(entityRow); // This will create the client side version of all the properties associated with this document. foreach (PropertyStoreInfo propertyStoreInfo in createDocumentInfo.Properties) { DataModel.PropertyStoreRow viewerPropertyRow = DataModel.PropertyStore.NewRow() as DataModel.PropertyStoreRow; viewerPropertyRow.EntityId = createDocumentInfo.EntityId; viewerPropertyRow.PropertyId = propertyStoreInfo.PropertyId; viewerPropertyRow.PropertyStoreId = propertyStoreInfo.PropertyStoreId; viewerPropertyRow.RowVersion = 0; viewerPropertyRow.Value = propertyStoreInfo.Value; DataModel.PropertyStore.Rows.Add(viewerPropertyRow); } // Finally, we need a relation built between the parent container and this document. DataModel.EntityTreeRow entityTreeRow = DataModel.EntityTree.NewRow() as DataModel.EntityTreeRow; entityTreeRow.ParentId = parentItem.EntityId; entityTreeRow.ChildId = createDocumentInfo.EntityId; entityTreeRow.EntityTreeId = createDocumentInfo.EntityTreeId; entityTreeRow.RowVersion = 0; DataModel.EntityTree.Rows.Add(entityTreeRow); // The object will be added to the common data model from a background thread. ThreadPool.QueueUserWorkItem(DirectoryPage.CreateItemThread, createDocumentInfo); }
/// <summary> /// Add a descendant to the list of items managed by this collection. /// </summary> /// <param name="assetNetworkItem"></param> internal void AddDescendant(AssetNetworkItem assetNetworkItem) { // This list is used to watch for changes to the collection. this.entityItemMap.Add(assetNetworkItem.EntityId, assetNetworkItem); }