By the time you read this, I will be gone on the first of a series of holidays this summer. By the way, for that reason, don't expect any answers to comments for a while. I left the computer at home this time! I posted this in advance to ensure you have something worthwhile to chew on during my absence.
This is another stepping stone towards implementing Martin Schmid's proposal to display and navigate through all unconnected MEP connectors in the model that I mentioned in the discussion on retrieving MEP elements and connectors.
Another one of the steps is determining the Revit parent window and attaching a form to it. The latter enables us to set Revit as the parent window of the modeless form, ensuring that it stays on top of Revit when Revit is visible, and also that it is minimised when the user minimises Revit.
After we have determined the unconnected connectors, we populate a modeless form with their pertinent information, display it, and enable the user to double click on an element to zoom to it in Revit. The double click handling and zooming interaction still needs to be documented. For the moment, we will focus on implementing, populating and displaying the modeless form.
I implemented a modeless form named LooseConnectorNavigator. It is derived from a Form base class from the System.Windows.Forms namespace. It has one single DataGridView control added to it which is anchored to all four sides. Here is what it looks like with its properties in the Visual Studio form designer:
A data grid view is extremely easy to populate with an absolute minimum of coding, as we have already seen in Joel Karr's sample showing how to list linked elements. If you set up an appropriate data container whose elements expose certain public member properties, all you need to do is to specify the container as the data source of the data grid view, and it will not only automatically populate the rows of data, but even create the columns according to the available properties!
What's more, by using auto-implemented properties, the definition of the properties on the data item classes is reduced to just specifying their name and type and almost nothing else.
In this case, we wish to display the unconnected connectors. Each connector belongs to a specific Revit element, and in some circumstances we wish to present the element data independently of its connectors, so I implemented two separate data containers for each, a base ElementData class and a derived ConnectorData one. Both of them have a list of public properties whose main purpose is to define exactly what ultimately gets displayed in the data grid view. They also each have a constructor which populates their public properties, and a ToString method to stream them to a text file for logging purposes:
public class ElementData { public string Class { get; set; } public string Category { get; set; } public string Family { get; set; } public string Symbol { get; set; } public string Name { get; set; } public int Id { get; set; } public ElementData( Element e, Document doc ) { Class = e.GetType().Name; Category = ( null == e.Category ) ? string.Empty : e.Category.Name; ElementId typeId = e.GetTypeId(); ElementType elementType = ( null == typeId ) ? null : doc.get_Element( typeId ) as ElementType; FamilyInstance fi = e as FamilyInstance; Duct duct = e as Duct; if( null != duct ) { string s = duct.DuctType.Name; } Family = ( null != fi ) ? fi.Symbol.Family.Name : string.Empty; Symbol = ( null != fi ) ? fi.Symbol.Name : ( ( null != elementType ) ? elementType.Name : string.Empty ); Name = e.Name; Id = e.Id.IntegerValue; } public override string ToString() { string c = (0 == Category.Length) ? Class : Category; string fam = (0 == Family.Length) ? string.Empty : "'" + Family + "' "; return string.Format( "{0} {1}<{2} '{3}'>", c, fam, Id, Name ); } } public class ConnectorData : ElementData { XYZ _p; public string ConnectorType { get; set; } public string X { get; set; } public string Y { get; set; } public string Z { get; set; } public ConnectorData( Element e, Document doc, ConnectorType ct, XYZ p ) : base( e, doc ) { ConnectorType = ct.ToString(); _p = p; X = Util.RealString( p.X ); Y = Util.RealString( p.Y ); Z = Util.RealString( p.Z ); } public override string ToString() { return string.Format( "{0} connector at ({1},{2},{3}) on {4}", ConnectorType, X, Y, Z, base.ToString() ); } }
There are a few details here worth mentioning. For instance, I split the connector point into separate X, Y and Z coordinate values, because then I can later sort them based on these values, which gives me more sorting options and flexibility than if they were all lumped together into one single point data item. Furthermore, I save the coordinates as string values instead of double numbers, because later on I do not want to see reams of useless, confusing and unintelligible post-comma digits in my user interface.
Talking about sorting, by the way:
In the linked elements example mentioned above, we simply used a generic .NET List<> as a data container. That works fine, but does not automatically provide support for one additional neat feature available from the data grid view, which is the ability to sort the columns.
If we use a sortable binding list instead, all of our columns will automatically be sortable. So we do just that.
Since most MEP models will initially contain a large number of parts, and many of them may have unconnected connectors, it makes sense to log the data to a text file in addition to presenting it in the modeless dialogue box. The text file is easier than a form to manually search or automatically post-process for specific purposes. Here is my log file implementation:
class JtLogFile : IDisposable { string _path; StreamWriter _sw; public JtLogFile( string basename ) { _path = System.IO.Path.Combine( System.IO.Path.GetTempPath(), basename + ".log" ); _sw = new StreamWriter( _path, true ); _sw.WriteLine( "\n\rStart analysis {0}\n\r", DateTime.Now.ToString( "u" ) ); } public void Dispose() { _sw.Close(); _sw.Dispose(); } public void Log( string s ) { _sw.WriteLine( s ); Debug.WriteLine( s ); } public string Path { get { return _path; } } }
We now have all the components ready to collect and display the unconnected connector information, both in the log file and the modeless form. Enter the external command mainline Execute method, which performs the following steps:
There is whole bunch of additional category tracking going on for logging and validation purposes. At the end of the log file, we display information on the number of elements and connectors processed and what categories they belong to. Don't let that confuse or distract you from the main points.
public Result Execute( ExternalCommandData commandData, ref String message, ElementSet elements ) { // set up IWin32Window instance encapsulating // main Revit application window handle: if( null == _hWndRevit ) { Process process = Process.GetCurrentProcess(); IntPtr h = process.MainWindowHandle; _hWndRevit = new JtWindowHandle( h ); } UIApplication app = commandData.Application; Document doc = app.ActiveUIDocument.Document; bool include_wires = false; SortableBindingList<ConnectorData> data = new SortableBindingList<ConnectorData>(); string path; using( JtLogFile log = new JtLogFile( "LooseConnectors" ) ) { FilteredElementCollector collector = GetConnectorElements( doc, include_wires ); ConnectorSet connectors = null; Dictionary<string, List<Element>> categories = new Dictionary<string, List<Element>>(); int nErrors = 0; int nUnconnected = 0; foreach( Element e in collector ) { Category cat = e.Category; Debug.Assert( null != cat, "expected a valid category on all elements" ); string key = cat.Name; if( !categories.ContainsKey( key ) ) { categories[key] = new List<Element>(); } categories[key].Add( e ); connectors = null; try { connectors = GetConnectors( e ); } catch( Exception ex ) { ++nErrors; log.Log( string.Format( "Error {0} retrieving connectors " + "from {1}: {2} '{3}'", nErrors, new ElementData( e, doc ), ex.GetType().Name, ex.Message ) ); } if( null != connectors ) { foreach( Connector c in connectors ) { if( // this is too restrictive: // ConnectorType.PhysicalConn // == c.ConnectorType // i had to add some strange checks to avoid // IsConnected throwing an exception: ConnectorType.LogicalConn != c.ConnectorType && 32 != ((int)c.ConnectorType) && !c.IsConnected ) { ++nUnconnected; ConnectorData cd = new ConnectorData( e, doc, c.ConnectorType, c.Origin ); log.Log( string.Format( "Unconnected {0}: {1}", nUnconnected, cd ) ); data.Add( cd ); } } } } int total = categories.Values .Aggregate<List<Element>, int>( 0, ( n, a ) => n + a.Count ); log.Log( string.Format( "Examined {0} elements of {1} categories:", total, categories.Count ) ); List<string> keys = new List<string>( categories.Keys ); keys.Sort(); foreach( string key in keys ) { Element e = categories[key][0]; BuiltInCategory bic = ( BuiltInCategory ) e.Category.Id.IntegerValue; log.Log( string.Format( "{0,8} '{1}' {2}", categories[key].Count, key, bic ) ); } log.Log( string.Format( "Error retrieving connectors on {0} elements," + " {1} unconnected connectors found.", nErrors, nUnconnected ) ); path = log.Path; } // display log file: Process.Start( path ); // display data in modeless form and ensure // that the form remains on tp of Revit: LooseConnectorNavigator navigator = new LooseConnectorNavigator( data, new SetElementId( SetPendingElementId ) ); navigator.Show( _hWndRevit ); // subscribe to Idling event: app.Idling += new EventHandler<IdlingEventArgs>( OnIdling ); return Result.Succeeded; }
Notes:
Here are a couple of sample lines from the generated log file listing the unconnected connector information in the rme_basic_sample_project.rvt included in the Revit MEP distribution:
Unconnected 48: EndConn connector at (77.22,-12.31,10.38) on Ducts <411599 'Mitered Elbows / Taps'> Unconnected 49: EndConn connector at (106.09,-12.31,10.38) on Ducts <411618 'Mitered Elbows / Taps'> Unconnected 50: EndConn connector at (136.28,-9.69,10.38) on Ducts <411724 'Mitered Elbows / Taps'>
This is the summary information listed at the end of the log file:
Examined 3927 elements of 15 categories: 309 'Air Terminals' OST_DuctTerminal 14 'Conduit Fittings' OST_ConduitFitting 20 'Conduits' OST_Conduit 933 'Duct Fittings' OST_DuctFitting 727 'Ducts' OST_DuctCurves 29 'Electrical Equipment' OST_ElectricalEquipment 424 'Electrical Fixtures' OST_ElectricalFixtures 63 'Lighting Devices' OST_LightingDevices 410 'Lighting Fixtures' OST_LightingFixtures 47 'Mechanical Equipment' OST_MechanicalEquipment 464 'Pipe Fittings' OST_PipeFitting 467 'Pipes' OST_PipeCurves 11 'Plumbing Fixtures' OST_PlumbingFixtures 3 'Specialty Equipment' OST_SpecialityEquipment 6 'Sprinklers' OST_Sprinklers Error retrieving connectors on 3 elements, 381 unconnected connectors found.
The modeless form populated and displayed by the command looks like this:
The next and final instalment of this discussion will present the details of the interaction of the modeless dialogue box with Revit. The modeless form does not have access to the Revit API, since it is not asynchronously accessible. Happily, we can use the Idling event to create a reliable and seamless solution for that.
In spite of not having completed the entire discussion yet, there is nothing to stop me from sharing the Visual Studio solution and source code with you already now, so here it is in loose_connectors_5.zip. This is actually almost the same sample that I already shared in the discussion of hooking up a modeless dialogue with the Revit parent window.
Enjoy, and I look forward to hearing back from you after my holidays!