Journal time-stamps at a set interval for another #RTCEUR #Revit API wish

Joel wished for “a time stamp added to journal files. I’m thinking something like an addin or macro that just notes the current time at specified intervals. The use would be for tracking performance – what processes take a long time, and how much how much time.”

It is so great that C# has the Timer class to make this easy – https://www.dotnetperls.com/timer

time-stamp

Application app = null;
public void JournalCommentTimer()
{
    app = this.Application;
    UIApplication uiapp = new UIApplication(app);
    double seconds = 10;
    Timer timer = new Timer(seconds * 1000);
    timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
    timer.Enabled = true;
}

void timer_Elapsed(object sender, ElapsedEventArgs e)
{
    app.WriteJournalComment("New Timestamp", true);
}

Add this to your file to compile the code above:
using System.Timers;

Purge unused materials for another #RTCEUR API wish

Yesterday I looked at how to purge unused families and family types. Purging materials needs a different approach because they are not used directly in the project but are referenced by other objects.

Here is an approach that uses the DocumentChanged event, rolling back Transaction Groups, and other good things do find materials that can be deleted without elements being modified. It takes a bit of time to run because it is deleting all materials and undoing the deletions that modify other elements.

material-purge

public static int modifiedByDeleteMaterial = 0;
public static bool checkForPurgeMaterials = false;
public static Document _doc = null;
public static string materialName = "";
public void purgeMaterials()
{
    Document doc = this.ActiveUIDocument.Document;
    _doc = doc;
    Application app = doc.Application;
    app.DocumentChanged += documentChanged_PurgeMaterials;
    List<Element> materials = new FilteredElementCollector(doc).OfClass(typeof(Material)).ToList();
    string deletedMaterials = "";
    int unusedMaterialCount = 0;
    foreach (Element material in materials)
    {
        modifiedByDeleteMaterial = 0;
        materialName = material.Name + " (id " + material.Id + ")";
        using (TransactionGroup tg = new TransactionGroup(doc, "Delete Material: " + materialName))
        {
            tg.Start();
            using (Transaction t = new Transaction(doc, "delete material"))
            {
                t.Start();
                checkForPurgeMaterials = true;
                doc.Delete(material.Id);
                
                // commit the transaction to trigger the DocumentChanged event
                t.Commit();
            }
            checkForPurgeMaterials = false;
            
            if (modifiedByDeleteMaterial == 1)
            {
                unusedMaterialCount++;
                deletedMaterials += materialName + Environment.NewLine;
                tg.Assimilate();
            }
            else // rollback the transaction group to undo the deletion
                tg.RollBack();
        }
    }
    
    TaskDialog td = new TaskDialog("Info");
    td.MainInstruction = "Deleted " + unusedMaterialCount + " materials";
    td.MainContent = deletedMaterials;
    td.Show();

    app.DocumentChanged -= documentChanged_PurgeMaterials;
}

private static void documentChanged_PurgeMaterials(object sender, Autodesk.Revit.DB.Events.DocumentChangedEventArgs e)
{
    // do not check when rolling back the transaction group
    if (!checkForPurgeMaterials)
    {
        return;
    }
    
    List<ElementId> deleted = e.GetDeletedElementIds().ToList();
    List<ElementId> modified = e.GetModifiedElementIds().ToList();
    
    // for debugging
    string s = "";
    foreach (ElementId id in modified)
    {
        Element modifiedElement = _doc.GetElement(id);
        s += modifiedElement.Category.Name + " " + modifiedElement.Name + " (" +  id.IntegerValue + ")" + Environment.NewLine;
    }
    //TaskDialog.Show("d", materialName + Environment.NewLine + "Deleted = " + deleted.Count + ", Modified = " + modified.Count + Environment.NewLine + s);
    
    // how many elements were modified and deleted when this material was deleted?
    // if 1, then the material is unused and should be deleted
    modifiedByDeleteMaterial = deleted.Count + modified.Count;
}

#RTCEUR Wish 2: Pipe Split and Hanger Creation

Brian wished for “splitting MEP Pipework at certain distances on the horizontals with Revit MEP Hangers”. Here’s a look at how the API can be used to create a new set of pipes of a specified length to replace one long pipe. Hangers are placed at the start and end of each new pipe.

One opportunity for improvement would be to do something more sophisticated at the end of the original pipe. If your pipe comes in standard lengths, you may not want to end up with a 1′ piece of pipe at the end if you need 37′ total feet of pipe and your standard length is 6′. If that sounds like an interesting problem to solve, send me a tweet and maybe I’ll post the solution tomorrow. But now its time to find out more about the PORT in Porto.

public void SplitPipe()
{
    UIDocument uidoc = this.ActiveUIDocument;
    Document doc = uidoc.Document;
    Pipe pipe = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element)) as Pipe;
    ElementId levelId = pipe.get_Parameter(BuiltInParameter.RBS_START_LEVEL_PARAM).AsElementId();

    Line pipeLine = ((LocationCurve)pipe.Location).Curve as Line;
    double segmentLength = 6;
    List<Line> newLines = new List<Line>();
    double i = 0;
    for (i=0; i <= pipeLine.Length - segmentLength; i = i + segmentLength)
    {
        XYZ end0 = pipeLine.Evaluate(i, false);
        XYZ end1 = pipeLine.Evaluate(i + segmentLength, false);
        newLines.Add(Line.CreateBound(end0, end1));
    }
    newLines.Add(Line.CreateBound(pipeLine.Evaluate(i, false), pipeLine.Evaluate(1, true)));
    
    FamilySymbol hangerSymbol = new FilteredElementCollector(doc)
        .OfClass(typeof(FamilySymbol))
        .Cast<FamilySymbol>()
        .FirstOrDefault(q => q.Family.Name == "Hanger" &&
                        q.Family.FamilyCategory.Id.IntegerValue == (int)BuiltInCategory.OST_PipeAccessory);
    
    using (Transaction t = new Transaction(doc, "Split Pipe"))
    {
        t.Start();
        View3D view3d = View3D.CreateIsometric(doc, new FilteredElementCollector(doc)
                                               .OfClass(typeof(ViewFamilyType))
                                               .Cast<ViewFamilyType>()
                                               .FirstOrDefault(q => q.ViewFamily == ViewFamily.ThreeDimensional).Id);
        
        List<Tuple<FamilyInstance, double>> hangers = new List<Tuple<FamilyInstance, double>>();
        for (int lineCtr = 0; lineCtr < newLines.Count; lineCtr++)
        {
            Pipe newPipe = Pipe.Create(doc, pipe.MEPSystem.GetTypeId(), pipe.PipeType.Id, levelId, newLines[lineCtr].GetEndPoint(0), newLines[lineCtr].GetEndPoint(1));
            ReferenceIntersector ri = new ReferenceIntersector(new ElementClassFilter(typeof(Ceiling)), FindReferenceTarget.Face, view3d);
            ReferenceWithContext rwc = ri.FindNearest(newLines[lineCtr].GetEndPoint(1), XYZ.BasisZ);
            if (rwc != null)
            {
                Reference r = rwc.GetReference();
                Ceiling ceiling = doc.GetElement(r) as Ceiling;
                Options opt = new Options();
                opt.ComputeReferences = true;
                PlanarFace ceilingBottomFace = null;
                Solid s = ceiling.get_Geometry(opt).Cast<GeometryObject>().FirstOrDefault(q => q is Solid) as Solid;
                foreach (Face face in s.Faces)
                {
                    if (face is PlanarFace)
                    {
                        PlanarFace pf = face as PlanarFace;
                        if (pf.FaceNormal.IsAlmostEqualTo(XYZ.BasisZ.Negate()))
                        {
                            
                            ceilingBottomFace = pf;
                            break;
                        }
                    }
                }
                if (ceilingBottomFace != null)
                {
                    FamilyInstance fiHanger = doc.Create.NewFamilyInstance(ceilingBottomFace, newLines[lineCtr].GetEndPoint(1), pipeLine.Direction.CrossProduct(XYZ.BasisZ), hangerSymbol);
                    hangers.Add(new Tuple<FamilyInstance, double>(fiHanger, rwc.Proximity));
                    if (lineCtr == 0)
                    {
                        fiHanger = doc.Create.NewFamilyInstance(ceilingBottomFace, newLines[lineCtr].GetEndPoint(0), pipeLine.Direction.CrossProduct(XYZ.BasisZ), hangerSymbol);    
                        hangers.Add(new Tuple<FamilyInstance, double>(fiHanger, rwc.Proximity));
                    }
                }
            }
        }
        
        foreach (Tuple<FamilyInstance, double> tup in hangers)
        {
            tup.Item1.Parameters.Cast<Parameter>().FirstOrDefault(q => q.Definition.Name == "Distance from Ceiling to Pipe Center").Set(tup.Item2);
        }
        
        doc.Delete(pipe.Id);
        doc.Delete(view3d.Id);
        t.Commit();
    }
}

#RTCEUR API Wish #1: Purging Types and Families

The first wish granted from Porto is for Phil who wished for “a solution to purging unused Revit elements from a Revit model”. Different elements need to be purged in different ways. This code will clean up unused system and loadable families and their types.

How are you wishing to make Revit better?

purge-browser

The “RollbackIfErrorOccurs” section is used to handle the case where deleting a wall type would result in this error.

last-type-in-system-family

public void purgeFamiliesAndTypes()
{
    Document doc = this.ActiveUIDocument.Document;
    
    // List of categories whose families will be purged
    List<int> categoriesToPurge = new List<int>();
    categoriesToPurge.Add((int)BuiltInCategory.OST_StructuralFraming);
    categoriesToPurge.Add((int)BuiltInCategory.OST_Walls);

    List<ElementId> typesToDelete = new List<ElementId>();

    // Check all element types whose category is contained in the category list	
    foreach (ElementType et in new FilteredElementCollector(doc)
             .OfClass(typeof(ElementType))
             .Cast<ElementType>()
             .Where(q => q.Category != null && 
                    categoriesToPurge.Contains(q.Category.Id.IntegerValue)))
    {
        // if there are no elements with this type, add it to the list for deletion
        if (new FilteredElementCollector(doc)
            .WhereElementIsNotElementType()
            .Where(q => q.GetTypeId() == et.Id).Count() == 0)
        {
            typesToDelete.Add(et.Id);
        }
    }
        
    using (TransactionGroup tg = new TransactionGroup(doc, "Purge families"))
    {
        tg.Start();
        foreach (ElementId id in typesToDelete)
        {    
            using (Transaction t = new Transaction(doc, "delete type"))
            {
                // Do not delete type if it would result in error such as
                // "Last type in system family "Stacked Wall" cannot be deleted."
                FailureHandlingOptions failOpt = t.GetFailureHandlingOptions();
                failOpt.SetClearAfterRollback(true);
                failOpt.SetFailuresPreprocessor(new RollbackIfErrorOccurs());
                t.SetFailureHandlingOptions(failOpt);
                
                t.Start();
                try
                {
                    doc.Delete(id);
                }
                catch
                {}
                t.Commit();
            }    
        }
        
        // Delete families that now have no types
        IList<ElementId> familiesToDelete = new List<ElementId>();    
        foreach (Family family in new FilteredElementCollector(doc)
            .OfClass(typeof(Family))
            .Cast<Family>()
            .Where(q => categoriesToPurge.Contains(q.FamilyCategory.Id.IntegerValue)))
        {
            // add family to list if there are no instances of any type of this family
            if (new FilteredElementCollector(doc)
                .OfClass(typeof(FamilyInstance))
                .Cast<FamilyInstance>()
                .Where(q => q.Symbol.Family.Id == family.Id)
                .Count() == 0)
            {
                familiesToDelete.Add(family.Id);
            }
        }
        
        using (Transaction t = new Transaction(doc, "delete families with no types"))
        {
            t.Start();
            doc.Delete(familiesToDelete);
            t.Commit();
        }
        
        tg.Assimilate();
    }
}

public class RollbackIfErrorOccurs : IFailuresPreprocessor
{
    public FailureProcessingResult PreprocessFailures(FailuresAccessor failuresAccessor)
    {
        // if there are any failures, rollback the transaction
        if (failuresAccessor.GetFailureMessages().Count > 0)
        {
            return FailureProcessingResult.ProceedWithRollBack;
        }
        else
        {
            return FailureProcessingResult.Continue;
        }
    }
}        

#RTCNA wish granted – Design Option Set info

Rabi asked

I am curious if you have figured a way to get the Design Options Set from Revit API

Here’s what it seems we can do with the parameters OPTION_SET_ID and PRIMARY_OPTION_ID

Capture

public void getDesignOptionSet()
{
    Document doc = this.ActiveUIDocument.Document;
    List<ElementId> setIds = new List<ElementId>();
    foreach (DesignOption dopt in new FilteredElementCollector(doc).OfClass(typeof(DesignOption)).Cast<DesignOption>())
    {
        ElementId setId = dopt.get_Parameter(BuiltInParameter.OPTION_SET_ID).AsElementId();
        if (!setIds.Contains(setId))
            setIds.Add(setId);
    }
    
    string data = "";
    foreach (ElementId id in setIds)
    {
        Element e = doc.GetElement(id);
        data += e.Name + " - " + doc.GetElement(e.get_Parameter(BuiltInParameter.PRIMARY_OPTION_ID).AsElementId()).Name + Environment.NewLine;
    }
    
    TaskDialog.Show("data",data);
}

#RTCNA wish almost granted – clear keynote when duplicating types

Clearing out my RTC API Wishlist Inbox… (do you have more wishes? RTCEUR is coming in 3 months)

Nicklas asks

Is it possible to create a macro that resets keynote when a family is duplicated?

The code and video below shows how Dynamic Model Update can be used to clear the Keynote parameter’s value any time a new Element Type is added to the file. But the problem with this is that it does not distinguish the different ways that a new Element Type can be added.

You can duplicate a type (the scenario that Nicklas wants to capture) but it also triggers when you load a family (a scenario that probably should not clear the keynote values for the just loaded families). The solution to this might include subscribing to the DocumentChanged event and using the GetTransactionNames Method to differentiate between a Duplicate and Load Family.

public void addTrigger()
{
    Application app = this.Application;
    MyUpdater udpater = new MyUpdater(app.ActiveAddInId);
    UpdaterRegistry.RegisterUpdater(udpater, true);
    UpdaterRegistry.AddTrigger(udpater.GetUpdaterId(), 
                               new ElementClassFilter(typeof(ElementType)), Element.GetChangeTypeElementAddition());
}

public class MyUpdater : IUpdater
{
    static AddInId m_appId;
    static UpdaterId m_updaterId;
    public MyUpdater(AddInId id)
    {
        m_appId = id;
        m_updaterId = new UpdaterId(m_appId, new Guid("FB1BA6B2-4C06-42d4-97C1-D1B4EB593EFA"));
    }
    public void Execute(UpdaterData data)
    {
        Document doc = data.GetDocument();
        try
        {
            foreach (ElementId id in data.GetAddedElementIds())
            {
                ElementType etype = doc.GetElement(id) as ElementType;
                if (etype == null)
                    continue;
                Parameter p = etype.get_Parameter(BuiltInParameter.KEYNOTE_PARAM);
                if (p == null)
                    continue;
                p.Set("");
            }
            
        }
        catch (Exception ex)
        {
            string s = ex.Message;
            TaskDialog td = new TaskDialog("MyUpdater Exception");
            td.MainInstruction = ex.Message;
            td.MainContent = ex.StackTrace;
            td.Show();
        }
    }
    public string GetAdditionalInformation() { return ""; }
    public ChangePriority GetChangePriority() { return ChangePriority.FloorsRoofsStructuralWalls; }
    public UpdaterId GetUpdaterId() { return m_updaterId; }
    public string GetUpdaterName() { return "MyUpdater"; }
}

#RTCNA2016 Wish Granted – Load Group from RVT

bd5cents sent in an API wish to:

Load Group in to project from external Group file. .rvt file.

This could be particularly useful if you have a folder full of Group RVTs and want to load them all, or if you want to create a dialog box where the user can select multiple groups to load.

Unfortunately, the API does not have a simple Document.LoadGroup() method, so we have to do a bit more work.

public void LoadModelGroup()
{
    string groupFile = @"C:\Users\harry_000\Documents\MyGroup.rvt";
    Application app = this.Application;
    
    Document doc = this.ActiveUIDocument.Document;
    
    // find elements to copy
    // to find the model elements in the group file,
    // create a 3d view and then get all the elements in that view
    Document docGroup = app.OpenDocumentFile(groupFile);
    ICollection<ElementId> toCopy = null;
    using (Transaction t = new Transaction(docGroup, "temp"))
    {
        t.Start();
        View3D view = View3D.CreateIsometric(docGroup, new FilteredElementCollector(docGroup)
                             .OfClass(typeof(ViewFamilyType))
                             .Cast<ViewFamilyType>()
                             .FirstOrDefault(q => q.ViewFamily == ViewFamily.ThreeDimensional).Id);
        toCopy = new FilteredElementCollector(docGroup, view.Id).ToElementIds();
        t.RollBack();
    }
    
    using (Transaction t = new Transaction(doc, "Copy Group"))
    {
        t.Start();
        
        // paste the elements from the group file
        ICollection<ElementId> newIds = ElementTransformUtils.CopyElements(docGroup, toCopy, doc, Transform.Identity, new CopyPasteOptions());
        
        // group the newly pasted elements
        Group group = doc.Create.NewGroup(newIds);
        
        // set the name of the group type
        group.GroupType.Name = Path.GetFileNameWithoutExtension(groupFile);
        
        // delete the instance of the group
        doc.Delete(group.Id);
        
        t.Commit();
    }
    
    docGroup.Close(false);
}

#RTCNA2016 Wish 2 Granted (part 2)

The previous post showed how to create lines in an elevation view to visualize the view range of a plan view. Now let’s see how to let the user move those lines in the elevation view and update the plan view’s range.

public void setViewRangeByLine()
{
    string planName = "Level 1 View Range Test";

    Document doc = this.ActiveUIDocument.Document;
    
    ViewPlan viewPlan = new FilteredElementCollector(doc)
        .OfClass(typeof(ViewPlan))
        .Cast<ViewPlan>()
        .FirstOrDefault(q => q.Name == planName);
    if (viewPlan == null)
        return;
    
    CurveElement bottomCurve = new FilteredElementCollector(doc, doc.ActiveView.Id)
        .OfClass(typeof(CurveElement))
        .Cast<CurveElement>().FirstOrDefault(q => q.LineStyle.Name == "Bottom Clip Plane");
    if (bottomCurve == null)
        return;
    
    double bottomZ = bottomCurve.GeometryCurve.GetEndPoint(0).Z;

    PlanViewRange range = viewPlan.GetViewRange();
    using (Transaction t = new Transaction(doc, "Set view range"))
    {
        t.Start();
        range.SetOffset(PlanViewPlane.BottomClipPlane, bottomZ);
        viewPlan.SetViewRange(range);
        t.Commit();
    }
    
}

#RTCNA2016 Wish 2 Granted! (part one)

Timothy had this API wish:

View Ranges in Section to show graphically like a space in section (with Interior checked on) . Allow me to drag the box up/down to level if desired otherwise keep “Level Above”.

Here is part one – creating lines in an elevation view to show the view range of a plan view.

public void showViewRange()
{
    string planName = "Level 1 View Range Test";
    
    Document doc = this.ActiveUIDocument.Document;
    
    ViewPlan viewPlan = new FilteredElementCollector(doc)
        .OfClass(typeof(ViewPlan))
        .Cast<ViewPlan>()
        .FirstOrDefault(q => q.Name == planName);
    if (viewPlan == null)
        return;
    
    PlanViewRange range = viewPlan.GetViewRange();
    Level bottomLevel = doc.GetElement(range.GetLevelId(PlanViewPlane.BottomClipPlane)) as Level;
    Level topLevel = doc.GetElement(range.GetLevelId(PlanViewPlane.TopClipPlane)) as Level;
    
    Category bottomClipLineStyle = doc.Settings.Categories.Cast<Category>().FirstOrDefault(q => q.Id.IntegerValue == (int)BuiltInCategory.OST_Lines)
        .SubCategories.Cast<Category>().FirstOrDefault(q => q.Name == "Bottom Clip Plane");

    Category topClipLineStyle = doc.Settings.Categories.Cast<Category>().FirstOrDefault(q => q.Id.IntegerValue == (int)BuiltInCategory.OST_Lines)
        .SubCategories.Cast<Category>().FirstOrDefault(q => q.Name == "Top Clip Plane");
    
    using (Transaction t = new Transaction(doc, "Make View Range Line"))
    {
        t.Start();
        if (bottomLevel != null)
        {
            double bottomOffset = range.GetOffset(PlanViewPlane.BottomClipPlane);
            double z = bottomLevel.Elevation + bottomOffset;
            DetailCurve bottomLine = doc.Create.NewDetailCurve(doc.ActiveView, Line.CreateBound(new XYZ(0,0,z), new XYZ(30,0,z)));
            if (bottomClipLineStyle != null)
            {
                bottomLine.LineStyle = bottomClipLineStyle.GetGraphicsStyle(GraphicsStyleType.Projection);
            }
        }
        
        if (topLevel != null)
        {
            double topOffset = range.GetOffset(PlanViewPlane.TopClipPlane);
            double z = topLevel.Elevation + topOffset;
            DetailCurve topLine = doc.Create.NewDetailCurve(doc.ActiveView, Line.CreateBound(new XYZ(0,0,z), new XYZ(30,0,z)));
            if (topClipLineStyle != null)
            {
                topLine.LineStyle = topClipLineStyle.GetGraphicsStyle(GraphicsStyleType.Projection);
            }
        }
        t.Commit();
    }
    
}

#RTCEUR Wish 5! View Template (partially) exported

T.R. wished for the ability to “compare two view templates to identify/highlight differences. Or a way to spit them to txt/xls for compare”

We can get the data from all parameters stored as numbers, text, and element ids. We can also query the properties of the View class. In the sample code, this is done using reflection so code does not need to be written for each property individually.

Also, GetNonControlledTemplateParameterIds returns a list of parameters that are not marked as included when this view is used as a template. GetTemplateParameterIds returns a list of parameter ids that may be controlled when this view is assigned as a template.

Sample output of all parameter values:

------PARAMETERS---------
Category,-2000279
Category,-2000279
Color Scheme Location,1
Design Option,-1
Detail Level,3
Discipline,1
Display Model,0
Edited by,
Family,
Family,
Family and Type,
Family and Type,
Family Name,
Far Clipping,0
Learning Content,
Parts Visibility,1
Phase Filter,375
Scale Value    1:,100
Show Hidden Lines,1
Sun Path,0
Type,
Type Name,
View Scale,100
View Template,-1
Workset,172
------PROPERTIES---------
AnalysisDisplayStyleId,-1
AreAnalyticalModelCategoriesHidden,False
AreAnnotationCategoriesHidden,False
AreImportCategoriesHidden,False
AreModelCategoriesHidden,False
ArePointCloudsHidden,False
AssemblyInstanceId,-1
AssociatedAssemblyInstanceId,-1
CanBePrinted,False
CreatedPhaseId,-1
DemolishedPhaseId,-1
GroupId,-1
Id,9610
IsAssemblyView,False
IsTemplate,True
IsValidObject,True
LevelId,-1
Name,Architectural Section
OwnerViewId,-1
Pinned,False
RevealConstraintsMode,False
Title,Architectural Section
UniqueId,96dd64a0-c5ef-4337-8f9b-658b5c420b33-0000258a
ViewName,Architectural Section
ViewSpecific,False
ViewTemplateId,-1
---------------
Enable Sketchy Lines,False
Sketchy Line Extension,0
Sketchy Line Jitter,0
public void exportViewTemplate()
{
    Document doc = this.ActiveUIDocument.Document;
    string templateName = "Architectural Section";
    View template = new FilteredElementCollector(doc).OfClass(typeof(View)).Cast<View>()
        .FirstOrDefault(q => q.IsTemplate == true && q.Name == templateName);
    
    using (StreamWriter sw = new StreamWriter(@"C:\Users\harry_000\Desktop\rtc\View Template Export - " + templateName + ".txt"))
    {
        // quick and easy solution to get data from simple parameters
        sw.WriteLine("------PARAMETERS---------");
        foreach (Parameter p in template.Parameters.Cast<Parameter>().OrderBy(q => q.Definition.Name))
        {
            string val = "<undefined>";
            if (p.StorageType == StorageType.Double)
                val = p.AsDouble().ToString();
            else if (p.StorageType == StorageType.Integer)
                val = p.AsInteger().ToString();
            else if (p.StorageType == StorageType.ElementId)
                val = p.AsElementId().IntegerValue.ToString();
            else if (p.StorageType == StorageType.String)
                val = p.AsString();
            
            if (val != "<undefined>")
                sw.WriteLine(p.Definition.Name + "," + val);
        }
        
        // use reflection to get values from all properties of the view class
        sw.WriteLine("------PROPERTIES---------");
        foreach (System.Reflection.PropertyInfo pi in template.GetType().GetProperties().OrderBy(q => q.Name))
        {
            string propertyName = "";
            string propertyValue = "";
            getPropertyNameAndValue(template, pi, out propertyName, out propertyValue);
            
            if (propertyValue != "<undefined>")
                sw.WriteLine(propertyName + "," + propertyValue);
        }
        
        sw.WriteLine("---------------");
        
        // complex parameters require more work to export values individually
        ViewDisplaySketchyLines sketchyLines = template.GetSketchyLines();
        sw.WriteLine("Enable Sketchy Lines," + sketchyLines.EnableSketchyLines.ToString());
        sw.WriteLine("Sketchy Line Extension," + sketchyLines.Extension.ToString());
        sw.WriteLine("Sketchy Line Jitter," + sketchyLines.Jitter.ToString());
        
    }
}

private void getPropertyNameAndValue(object o, System.Reflection.PropertyInfo pi, out string propertyName, out string propertyValue)
{
    propertyName = pi.Name;
    propertyValue = "<undefined>";
    if (pi.PropertyType == typeof(ElementId) ||
        pi.PropertyType == typeof(Boolean) ||
        pi.PropertyType == typeof(String) ||
        pi.PropertyType == typeof(Enum))
    {
        try
        {
            propertyValue = pi.GetValue(o).ToString();
        }
        catch
        {}
    }
}

#RTCEUR Wish 4 granted! Change parameter value in multiple families

Doug asked ” How about a routine to change the value of a shared parameter in all families in a folder to the same specified value.”

public void setParamInFamilies()
{
    Application app = this.Application;
    foreach (string filename in Directory.GetFiles(@"C:\Users\harry_000\Desktop\rtc", "*.rfa"))
    {
        Document doc = app.OpenDocumentFile(filename);
        using (Transaction t = new Transaction(doc, "Set param value"))
        {
            t.Start();
            
            // get the parameter to set
            FamilyParameter param = doc.FamilyManager.get_Parameter("RTC Parameter");
            
            if (param == null)
                continue;
            
            // loop through all types in the family
            foreach (FamilyType ftype in doc.FamilyManager.Types)
            {
                // set the parameter value for this type
                doc.FamilyManager.Set(param, "Budapest");
            }
            t.Commit();
            doc.Save();
            doc.Close(false);
        }
    }
}

#RTCEUR Wish #2 Granted! Rename views

Jason asked “How about a find / replace tool for view names?”

Wish granted!

public void viewRename()
{
    string find = "Level";
    string replace = "LEV";
    
    Document doc = this.ActiveUIDocument.Document;
    string errors = "";
    using (Transaction t = new Transaction(doc, "Rename views"))
    {
        t.Start();
        // find all views whose name contains the "find" string
        foreach (Element view in new FilteredElementCollector(doc).OfClass(typeof(View))
                 .Where(q => q.Name.Contains(find)))
        {
            try
            {
                view.Name = view.Name.Replace(find, replace);
            }
            catch // error handling if there is already a view with what would be the new name
            {
                errors += view.Name + ", ";
            }
        }
        t.Commit();
     }
    
    // show error dialog if there are errors
    if (errors != "")
    {
        TaskDialog td = new TaskDialog("Error");
        td.MainInstruction = "Could not rename views because duplicate names would be created";
        td.MainContent = errors;
        td.Show();
    }
}

#RTCEUR Wish 1 granted!

Chris asked how the API could help copy a legend to multiple sheets. Wish granted!

public void legendOnSheets()
{
    Document doc = this.ActiveUIDocument.Document;
    
    // create list of element ids for the sheets that will get the legends
    List<ElementId> sheetIds = new List<ElementId>();
        
    // use SheetNumber to find the desired sheets
    // add each sheet to the list of sheet ids
    sheetIds.Add(new FilteredElementCollector(doc)
        .OfClass(typeof(ViewSheet))
        .Cast<ViewSheet>()
        .FirstOrDefault(q => q.SheetNumber == "A101").Id);
    sheetIds.Add(new FilteredElementCollector(doc)
        .OfClass(typeof(ViewSheet))
        .Cast<ViewSheet>()
        .FirstOrDefault(q => q.SheetNumber == "A102").Id);                
    sheetIds.Add(new FilteredElementCollector(doc)
        .OfClass(typeof(ViewSheet))
        .Cast<ViewSheet>()
        .FirstOrDefault(q => q.SheetNumber == "A103").Id);    
    
    // find the legend to put on the sheets
    // use ViewType.Legend and the view name to find the legend view
    Element legend = new FilteredElementCollector(doc)
        .OfClass(typeof(View))
        .Cast<View>()
        .FirstOrDefault(q => q.ViewType == ViewType.Legend && q.Name == "Legend 1");
    
    // create a transaction so that the document can be modified
    using (Transaction t = new Transaction(doc, "Legends on Sheets"))
    {
        // start the transaction
        t.Start();
        
        // loop through the list of sheet ids
        foreach (ElementId sheetid in sheetIds)
        {
            // create a new viewport for the legend on each sheet
            // place the legend view at the 0,0,0 location on each sheet
            // user will need to move the legend to the desired location
            Viewport.Create(doc, sheetid, legend.Id, new XYZ(0,0,0));
        }
        
        // commit the changes
        t.Commit();
    }
}

#RTCNA leftover – know your StorageType

A question came up at RTC about why a certain parameter could not be set with the API. Turns out that it was a parameter that stored an ElementId. In this case, trying to set the parameter with a string for the name of the desired element does not work. And to make it confusing, it fails silently with no warning, error, or exception.

When the parameter was set with an ElementId, all was well. See the example below where we need to use the ID of the Phase named “Existing” instead of just setting the parameter to the string “Existing”.

public void setPhase()
{
    Document doc = this.ActiveUIDocument.Document;
    Parameter phaseParam = doc.ActiveView.get_Parameter(BuiltInParameter.VIEW_PHASE);
    using (Transaction t = new Transaction(doc, "Set Phase"))
    {
        t.Start();
        
        // doesn't work, because phaseParam stores an ElementId, not a string
        // see the Parameter.StorageType Property for more info
        // phaseParam.Set("Existing");
        
        phaseParam.Set(new FilteredElementCollector(doc).OfClass(typeof(Phase)).FirstOrDefault(q => q.Name == "Existing").Id);
        
        t.Commit();
    }
}

#RTCNA wish 10 granted! Material replacement

Amy asked if the API can be used to find and replace materials

This implementation does that for wall types and could be extended for other objects where the material is stored in a different manner.

BEFORE

 before

AFTER

after


public void replaceMaterials()
{
    Document doc = this.ActiveUIDocument.Document;
    
    List<Tuple<string,string>> replacements = new List<Tuple<string, string>>();
    // old materal listed first, then new material that will replace it
    replacements.Add(new Tuple<string, string>("Brick, Common", "Brick, Orange"));
    replacements.Add(new Tuple<string, string>("Concrete Masonry Units", "Cherry"));

    using (Transaction t = new Transaction(doc, "Replace Materials"))
    {
        t.Start();
        foreach (WallType wt in new FilteredElementCollector(doc).OfClass(typeof(WallType)).Cast<WallType>())
        {
            CompoundStructure cs = wt.GetCompoundStructure();
            if (cs == null)
                continue;

            int idx = 0;
            foreach (CompoundStructureLayer layer in cs.GetLayers())
            {
                foreach (Tuple<string, string> tup in replacements)
                {
                    Element oldMaterial = new FilteredElementCollector(doc).OfClass(typeof(Material))
                        .FirstOrDefault(q => q.Name == tup.Item1);
                    Element newMaterial =  new FilteredElementCollector(doc).OfClass(typeof(Material))
                        .FirstOrDefault(q => q.Name == tup.Item2);
                    
                    if (layer.MaterialId == oldMaterial.Id)
                        cs.SetMaterialId(idx, newMaterial.Id);
                }
                idx++;
            }
            wt.SetCompoundStructure(cs);
        }
        t.Commit();
    }
}

#RTCNA wish 8 granted! Delete unused elevation markers

Want to get rid of these?

Capture

public void deleteUnusedElevations()
{
    Document doc = this.ActiveUIDocument.Document;
    using (Transaction t = new Transaction(doc, "Delete unused elevations"))
    {
        t.Start();
        doc.Delete(new FilteredElementCollector(doc)
                   .OfClass(typeof(ElevationMarker))
                   .Cast<ElevationMarker>()
                   .Where(q => !q.HasElevations())
                   .Select(q => q.Id).ToList());
        t.Commit();
    }
}

#RTCNA wish 7 granted! Isolated 3d view for each workset

Create a 3d view for each workset, and in that view isolate the display of the elements in that workset.

Note that to find worksets we use a FilteredWorksetCollector (not a FilteredElementCollector) and a WorksetKindFilter to get only the user worksets (don’t want view, family, project standard worksets).

public void ViewWorksetIso()
{
    Document doc = this.ActiveUIDocument.Document;
    if (!doc.IsWorkshared)
        return;
    
    // get the 3d view type which is needed when creating 3d views
    ViewFamilyType vft = new FilteredElementCollector(doc)
        .OfClass(typeof(ViewFamilyType))
        .Cast<ViewFamilyType>()
        .FirstOrDefault(q => q.ViewFamily == ViewFamily.ThreeDimensional);
    
    using (Transaction t = new Transaction(doc, "workset view isolation"))
    {
        t.Start();
        
        // loop through all worksets (but only User worksets)
        foreach (Workset wset in new FilteredWorksetCollector(doc).WherePasses(new WorksetKindFilter(WorksetKind.UserWorkset)))
        {
            // create a 3d view
            View3D view = View3D.CreateIsometric(doc, vft.Id);
            
            // set the name of the view to match the name of the workset
            view.Name = "WORKSET - " + wset.Name;
            
            // isolate elements in the view, using a filter to find elements only in this workset
            view.IsolateElementsTemporary(new FilteredElementCollector(doc).WherePasses(new ElementWorksetFilter(wset.Id)).Select(q => q.Id).ToList());
        }
        t.Commit();
    }
}

#RTCNA wish 6 granted! Create assemblies from groups

Here is how to create an assembly and assembly views from a group

public void assemblyFromGroup()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    
    // prompt the user to select a group
    Group group = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element)) as Group;
    
    View v3d = null;
    View partlist = null;
    View section = null;
    using (Transaction t = new Transaction(doc,"Create Assembly From Group"))
    {
        t.Start();
        
        // ungroup the group, getting the list of elements in the group
        List<ElementId> ids = group.UngroupMembers().ToList();
        
        // get the category of the first element in the group
        // a category needs to be specified when creating the assembly
        Category cat = doc.GetElement(ids.First()).Category;
        
        // create the assembly
        AssemblyInstance ai = AssemblyInstance.Create(doc, ids, cat.Id);
        
        // create the assembly views
        v3d = AssemblyViewUtils.Create3DOrthographic(doc, ai.Id);
        partlist = AssemblyViewUtils.CreatePartList(doc, ai.Id);
        section = AssemblyViewUtils.CreateDetailSection(doc, ai.Id, AssemblyDetailViewOrientation.DetailSectionA);
        t.Commit();
    }
    // activate the new views so they are visible when the command ends
    // The active view can only be changed when there is no open transaction
    uidoc.ActiveView = v3d;
    uidoc.ActiveView = partlist;
    uidoc.ActiveView = section;
}

#RTCNA wish 4 granted! Delete empty tags

Its great when a tiny bit of code can do something useful!

  • Filter for only elements in the active view: FilteredElementCollector(doc, doc.ActiveView.Id)
  • Get only tags with no text: Where(q => q.TagText == "")
  • Convert from Elements into a list of ElementIds (because that’s what Document.Delete() requires as an input): .Select(q => q.Id).ToList()

BEFORE

 before

AFTER

after

public void deleteEmptyTags()
{
    Document doc = this.ActiveUIDocument.Document;
    using (Transaction t = new Transaction(doc, "Delete empty tags"))
    {
        t.Start();
        doc.Delete(new FilteredElementCollector(doc, doc.ActiveView.Id)
             .OfClass(typeof(IndependentTag))
             .Cast<IndependentTag>()
             .Where(q => q.TagText == "")
             .Select(q => q.Id).ToList());
        t.Commit();
    }
}

#RTCNA Wish 3 granted! Join all walls and floors

Here’s how to find all wall/floor intersections and join their geometry. The first, simpler macro prompts the user to select to elements and then joins them. The 2nd one looks at each wall in the model and joins it to all intersecting floors.

BEFORE
before
AFTER
after

public void joinGeometryUserPick()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    
    // prompt user to select two elements
    Element e1 = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element));
    Element e2 = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element));
    
    // join the selected elements
    using (Transaction t = new Transaction(doc, "Join"))
    {
        t.Start();
        JoinGeometryUtils.JoinGeometry(doc, e1, e2);
        t.Commit();
    }
}

public void joinAllWallsFloors()
{
    Document doc = this.ActiveUIDocument.Document;
    using (Transaction t = new Transaction(doc, "Join All Walls/Floors"))
    {
        t.Start();
        foreach (Element wall in new FilteredElementCollector(doc)
             .OfClass(typeof(Wall)))
        {
            // get the bounding box of this wall
            BoundingBoxXYZ bbox = wall.get_BoundingBox(null);
            
            // create an outline from the min and max points of the bounding box
            Outline outline = new Outline(bbox.Min, bbox.Max);
            
            // find all floors that intersect the wall
            foreach (Element floor in new FilteredElementCollector(doc)
                 .OfClass(typeof(Floor))
                 .WherePasses(new BoundingBoxIntersectsFilter(outline)))
            {
                JoinGeometryUtils.JoinGeometry(doc, wall, floor);
            }
                     
        }
        t.Commit();
    }
}