Family Security Guard – AlwaysRequirePassword & AccessRestricted

Thanks to feedback from reader Arno, I’ve added two new features to this app. I hope the provides the flexibility and control that everyone – or almost everyone šŸ™‚ – would like. If not, please let me know.

  1. The password will be required to edit and open all family files if there is “AlwaysRequirePassword.txt” file in the BoostYourBIM-FamilySecurity.bundle\Contents folder.
  2. If this file does not exist, the password will only be required for families that contain a parameter named ā€œAccessRestrictedā€. The value of this parameter does not matter, only whether or not it exists.
Advertisement

FREE “Family Security Guard” submitted to Autodesk App Store

Thanks for all the great feedback I got from the recent posts about RFA security. I’ve submitted “Family Security Guard” to Autodesk to have it published (for FREE!). I will let you know when it is ready for download here.

The user will be prompted for a password in two situations:

  1. In the project environment when they attempt to open the Family Types dialog
  2. When they attempt to open a family file

After the password is successfully entered, the user will not be prompted again for the password during this Revit session.Ā The password can be set in the pwd.txt file in the BoostYourBIM-FamilySecurity.bundle\Contents folder.

NOTE: This app only protects families when this app is installed. There is no protection when someone without this app attempts to modify families. (I know many people would like to protect families all the time, but unfortunately Revit does not support this)

Family Parameter Editing in Excel – Data Round-trip

Yesterday’s post was about getting Family Parameter data out of Revit and into Excel. Here you can see:

  1. Parameter data exported to Excel for every family in a specified directory
    For this example I hard-coded “C:\ProgramData\Autodesk\RAC 2013\Libraries\US Imperial\Columns” but it could be any directory you want
  2. Data modified in Excel for multiple families
  3. Data read back into the family files
  4. Modified families saved to disk
  5. Families manually opened to show the changes made with the API commands

Cool?

Bulk Family Parameter Editing in Excel

After the recent discussions about the OmniClass and UniFormat parameters, I’ve been thinking about more generic tools optimized for modifying multiple parameters for multiple families.

One option would be to build a big grid-like UI as part of a Revit API app where the parameters could be edited.

Easier would be to export everything to Excel, let you make changes there, and then modify the parameters by reading back in the Excel file. The first half of this process (Export to Excel) could look like this:

In its current state, this isn’t usable for parameters that store element IDs (like Material). But if you could read this data into Revit to set text and numeric parameter values, would it be helpful?

public void FamilyParametersToExcel()
{
    Autodesk.Revit.ApplicationServices.Application application = this.Application;
    string directory = @"C:\ProgramData\Autodesk\RAC 2013\Libraries\US Imperial\Columns";
    string s = "";

    // loop through all files in the directory
    foreach (string filename in Directory.GetFiles(directory))
    {
        s += "Family Name,Family Type,";
        Document doc = application.OpenDocumentFile(filename);

        if (!doc.IsFamilyDocument)
            return; // skip any files that are not families

        FamilyManager familyManager = doc.FamilyManager;

        // sort the family parameters alphabetically
        List<string> paramSorted = new List<string>();
        foreach (FamilyParameter fp in familyManager.Parameters)
        {
            paramSorted.Add(fp.Definition.Name);
        }
        paramSorted.Sort();

        // create a header row listing the parameter names for this family
        foreach (string paramName in paramSorted)
        {
            FamilyParameter familyParameter = familyManager.get_Parameter(paramName);
            s += familyParameter.Definition.Name + ",";
        }
        s += "\n";

        // create a row for each family type
        foreach (FamilyType familyType in familyManager.Types)
        {
            if (familyType.Name == " ") // skip the 'default' family type with no name
                continue;

            s += doc.PathName + "," + familyType.Name + ",";

            foreach (string paramName in paramSorted)
            {
                FamilyParameter familyParameter = familyManager.get_Parameter(paramName);
                s += ParameterValueForType(familyType, familyParameter) + ",";
            }
            s += "\n";
        }
        s += "\n";                
        doc.Close(false);
    }

    // write the CSV file to disk
    string outfile = directory.Replace(@"\","-").Replace("C:","") + "-Parameters.";
    outfile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), outfile);
    using (StreamWriter streamWriter = new StreamWriter(outfile))
    {
        streamWriter.Write(s);
    }

    // open the CSV file
    Process.Start(outfile);
}

private string ParameterValueForType(FamilyType type, FamilyParameter p)
{
    // return a string with the value of the parameter for this type
    string paramValue = "";
    switch (p.StorageType)
    {
        case StorageType.Double:
            paramValue = type.AsDouble(p).ToString();
            break;
        case StorageType.ElementId:
            paramValue = type.AsElementId(p).IntegerValue.ToString();
            break;
        case StorageType.Integer:
            paramValue = type.AsInteger(p).ToString();
            break;
        case StorageType.String:
            paramValue = type.AsString(p);
            break;
    }
    if (paramValue == null)
        paramValue = "";

    return paramValue;
}
Ā 

OmniClass API app to create & set shared type parameters

OmniTypeParamsTo follow up my TwitterĀ conservationĀ withĀ Martijn and Aaron, here’s my take on what could be a useful app to create shared type parameters for OmniClass parameters

If you want to use the existing family-wide parameters “OmniClass Number” and “OmniClass Title” then select the “Built-In Family Parameters” radio button.

But if you want to use your own shared parameters so that each family type can have its own OmniClass values, then select the other radio button, select your shared parameter file, the family type, and the text parameters from that shared parameter file that shouldĀ receiveĀ the OmniClass data.

When you push the OK button, the two shared parameters would be created as Type parameters in the family (if they don’t already exist). The OmniCode data selected in the grid control would be set for those 2 parameters.

Is this what would be useful?

Assembly Code & Description – By Type or Family?

I’m putting the finishing touches on the app I mentioned hereĀ for setting the UniFormat values for Assembly Code & Assembly Description. The Revit UI hasĀ Ā Assembly Code & Description as Type parameters, so each type of a family can have different values.

But does anyone ever give each Type of the same family a different Assembly Code? Or do you want them to act more like the OmniCode parameters, where every Type has the same Assembly Code?

API / Journal Hybrid to Set Revisions

As great as the Revit API is, there are still things that it doesn’t fully support, such as helping solve this problem:

When you select one sheet, you have the option to edit the ā€œRevisionsĀ on Sheetā€ field.Ā  When you select multiple sheets, that options goes away

Here is a hybrid approach that uses both the API and Revit journal files.

  • The video starts with an API command where the user specifies the set of sheets to modify (from saved View/Sheet Sets in the RVT file) and the revision number.
  • Then (starting at 0:25 in the video) it uses a journal file to make the revision changes with input from data stored by the API command.
  • After the file loads (at 1:05 in the video) there is approximately 1 second of rapid selection in the project browser and in the Properties dialog. This is the journal replay.
  • At the end of the video I move the cursor and select the 3 sheets to show that that the new revision has been set.

Learn Keyboard Shortcuts with an API app

Would you like to use Revit faster? I learned about KeyRocket from posts at What Revit WantsĀ and better Revit. KeyRocket is great for Windows and Office, but it doesn’t know anything about Revit.

So I used the Revit API to make this app to help remind Revit users of the shortcuts. Any thoughts on this before I put it in the Autodesk App Store?

More on Family Security – require password to open family files

Yesterday’s post was about putting password protection on the Edit Type button in the Properties dialog. It was great to see so many comments and page views from people interested in this.

Here is another part of limiting access to modifying families – preventing unauthorized users from opening family files in the Family Editor.

This could be combined with the Edit Type password requirement shown yesterday but here are a few issues that would need to be resolved. It would be great to hear your ideas on these.

  1. Should these restrictions be applied to all families? If not, how should specific families be marked? Would a Yes/No type parameter in each family make sense? (It would be nice to able to add a family parameter instead of a type parameter, but the Revit does not support that.) Another option would be to mark families by storing some extensible storage data, but that introduces its own complexities.

  2. Should there be password restrictions on both opening existing families and creating new families?
  3. If I made this available as a single compiled application, everyone who downloads it would have the same application with the same password. Is this a deal-breaker?
    Ideally every company would be able to set their own password, but having me generate a unique DLL for each firm seems a bit cumbersome for everyone.

Search & Set App for OmniCode and UniFormat

I’ve put a searchable & sortable UI on the code I posted last week to improve the experience of setting these parameters. This probably isn’t something you are doing every day, but when you do I hope this will be a worthwhile improvement. It will be on the Autodesk App Exchange soon.

Dynamic Model Update, Exceptions, and Transactions (hint – look in the journal file)

In the last post I casually mentioned that during Dynamic Model Update we could delete the just-created elements. My first attempt was to add a transaction and Document.Delete(). That doesn’t work, and it also brings up a useful tip.

The error dialog shown on the right is Revit’s way of saying that your updater threw an exception, though it doesn’t give any technical details about the nature of the execption. “But what was the exception?” you will rightfully wonder! The answer is in the journal file, where in this case it explains:

' 2:< Exception caught from managed method RevitAPI::Autodesk.Revit.DB.TransactionStatus Start() <Autodesk.Revit.Exceptions.InvalidOperationException> <Starting a new transaction is not permitted. It could be because another transaction already started and has not been completed yet, or the document is in a state in which it cannot start a new transaction (e.g. during failure handling or a read-only mode, which could be either permanent or temporary).>

errorpublic void Execute(UpdaterData data)
{
    Document doc = data.GetDocument();
    Autodesk.Revit.ApplicationServices.Application app = doc.Application;
    foreach (ElementId addedElemId in data.GetAddedElementIds())
    {
        ImportInstance ii = doc.GetElement(addedElemId) as ImportInstance;
        if (ii.IsLinked == false)
            TaskDialog.Show("Hey!", app.Username + " - Maybe should should have linked that CAD instead of importing it.");

        // THIS DOESN'T WORK!
        Transaction t = new Transaction(doc,"delete that");
        t.Start();
        doc.Delete(ii);
        t.Commit();
    }
}

Thanks to that info in the journal file, we learn that we need to get rid of the transaction code. Doing so gives us this, which works fine to delete the import instance that was just created.

public void Execute(UpdaterData data)
{
    Document doc = data.GetDocument();
    Autodesk.Revit.ApplicationServices.Application app = doc.Application;
    foreach (ElementId addedElemId in data.GetAddedElementIds())
    {
        ImportInstance ii = doc.GetElement(addedElemId) as ImportInstance;
        if (ii.IsLinked == false)
            TaskDialog.Show("Hey!", app.Username + " - Maybe should should have linked that CAD instead of importing it.");
        doc.Delete(ii);
    }
}

Learn API Programming at Codeacadmey

The cool folks at Codeacademy have a new set of lessons to introduce API programming! It focuses on web APIs (instead of the .NET API framework that Revit uses) and gives a lot of great hands-on examples why APIs are so important and how they can be used in a variety of applications.

Maybe there are some cool intersections between these Codeacademy lessons and things we might want to do with Revit. For example, how about sending an email to your teammates when important changes are saved into the central file?

Application Programming Interface: A fancy way of saying “do more.”

Need a map for your site? Don’t code your own — use an API!

APIs let you talk to other web apps like Google Maps and Twitter so that you don’t have to build from scratch.

Simply use an API to borrow another app’s functionality. Easy!

Learn how to create a web app that can:

– Send text messages withĀ Twilio
– Pull videos fromĀ YouTube
– Search for songs onĀ SoundCloud
– Get the latest stories fromĀ NPR

The possibilities are as wide as the sky.

Image-O-Matic is now even better!

Thanks to the many people who tried Image-O-Matic today! I appreciate your feedback to help make it better.

I have made a couple improvements:

  1. Integer parameters are now supported. This allows you to modify array parameters in a family.
  2. The Min and Max values have been replaced with Start and End values. Start can be larger than End if you enter a negative increment.

The new version will hopefully posted on the Autodesk Exchange soon. In the meantime, you can get the new DLL atĀ https://docs.google.com/file/d/0BwszsfY3OsZHalhkX1BSTmgyYVE/editĀ and use it to replace the one you already have in the %APPDATA%\Autodesk\revit\Addins\2013\BoostYourBIM-ImageOMatic.bundle\Contents folder.

Quick way to renumber doors, grids, and family instances

A friend asked about a macro to renumberĀ grids in the order they are picked. Here’s how:


public void renumberByPicks()
{
    UIDocument uidoc = this.ActiveUIDocument;
    Document doc = uidoc.Document;

    // list that will contain references to the selected elements
    IList<Reference> refList = new List<Reference>();
    try
    {
        // create a loop to repeatedly prompt the user to select an element
        while (true)
            refList.Add(uidoc.Selection.PickObject(ObjectType.Element, "Select elements in order to be renumbered. ESC when finished."));
    }
    // When the user hits ESC Revit will throw an OperationCanceledException which will get them out of the while loop
    catch
    {}

    using (Transaction t = new Transaction(doc,"Renumber"))
    {
        t.Start();
        // need to avoid encountering the error "The name entered is already in use. Enter a unique name."
        // for example, if there is already a grid 2 we can't renumber some other grid to 2
        // therefore, first renumber everny element to a temporary name then to the real one
        int ctr = 1;
        int startValue = 0;
        foreach (Reference r in refList)
        {
            Parameter p = getParameterForReference(doc, r);

            // get the value of the first element to use as the start value for the renumbering in the next loop
            if (ctr == 1)
                startValue = Convert.ToInt16(p.AsString()); 

            setParameterToValue(p, ctr + 12345); // hope this # is unused (could use Failure API to make this more robust
            ctr++;
        }

        ctr = startValue;
        foreach (Reference r in refList)
        {
            Parameter p = getParameterForReference(doc, r);
            setParameterToValue(p, ctr);
            ctr++;
        }
        t.Commit();
    }

}
private Parameter getParameterForReference(Document doc, Reference r)
{
    Element e = doc.GetElement(r);
    Parameter p = null;
    if (e is Grid)
        p = e.get_Parameter("Name");
    else if (e is Room)
        p = e.get_Parameter("Number");
    else if (e is FamilyInstance)
        p = e.get_Parameter("Mark");
    else
    {
        TaskDialog.Show("Error","Unsupported element");
        return null;
    }
    return p;
}        
private void setParameterToValue(Parameter p, int i)
{
    if (p.StorageType == StorageType.Integer)
        p.Set(i);
    else if (p.StorageType == StorageType.String)
        p.Set(i.ToString());    
}

Search the Uniformat file & Set the Parameter

Steve makes a good observation in this post:

It would be nice if we could search for keywords when attempting to assign the parameters for both Omniclass and Uniformat. Sometimes the best category to use in Revit isn’t necessarily the correct category in these classification systems. This means a fair bit of time wasted hunting for an appropriate value. I search the source files to find keywords first which isn’t the most straightforward.

Here’s a proof-of-concept showing how it could be done without too much heavy lifting. I haven’t coded a UI for this as I mention in the source code comments, but getting the search string from the text note is a fine simplification to show what to do with the search string.

public void uniformatSearch()
{
    UIDocument uidoc = this.ActiveUIDocument;
    Document doc = this.ActiveUIDocument.Document;

    // select the element whose Uniformat parameter will be set
    Element e = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, "Select the element to set Assembly Code."));

    // select a text note whose text indicates the string to search for
    // in a real app there would be UI to enter this search string, but this works for a proof of concept macro
    TextNote text = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, "Select the text note with the search term.")) as TextNote;

    // the location of the Uniformat file
    string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
    string uniformatFile = Path.Combine(appData, @"Autodesk\revit\Autodesk Revit Architecture 2013", "UniformatClassifications.txt");

    IList<string> matches = new List<string>();
    // read the uniformat file one line at a time
    using (StreamReader read = new StreamReader(uniformatFile))
    {
        string line;
        while ((line = read.ReadLine()) != null)
       {
            // add the line to the list of matches if it contains the search string
            if (line.Contains(text.Text))
                matches.Add(line);
       }
    }
    // show all matches
    TaskDialog.Show("Matches for " + text.Text, listToString(matches));

    // get the type of the selected element and its uniformat parameter
    ElementType eType = doc.GetElement(e.GetTypeId()) as ElementType;
    Parameter p = eType.get_Parameter(BuiltInParameter.UNIFORMAT_CODE);

    // set the value of the parameter to the first entry in the match list - good enough for this proof-of-concept
    using (Transaction t = new Transaction(doc,"Set Uniformat"))
    {
        t.Start();
        // the uniformat file is a tab-delimited file
        // the string to use to set the parameter is the first entry [0] in the array created by splitting the complete string
        p.Set(matches.First().Split('\t')[0]);
        t.Commit();
    }
}
private string listToString(IList<string> stringList)
{
    string ret = "";
    foreach (string s in stringList)
    {
        ret += s.Replace("\t"," ") + "\n";
    }
    return ret;
}

Win a free, custom-built Revit API app for using Image-O-Matic!

To celebrate the posting of Image-O-Matic on the Autodesk Exchange, I am sponsoring a great contest!

  1. Download Image-O-Matic from the Autodesk Exchange and use it to make a great video showing the animation of a Revit family or phased model.
    For a Vasari installer, visitĀ https://boostyourbim.wordpress.com/products
  2. Email me for a free license so Image-O-Matic will be able to make large image files. Or purchase a license for only $4.99 atĀ https://boostyourbim.wordpress.com/products
  3. Upload your video to YouTube
  4. Leave a comment on this post with a link to your YouTube video

Contest endsĀ FebruaryĀ 15. Two winners will be chosen through a poll that I will post here after February 15 – one winner for phasing, one winner for instance parameters.

Each winner will get to design one Revit API app that I will build to their specifications!

The app should be approximately as complex as Image-O-Matic and I will collaborate with the winners to agree on the final design. Ā The winners will get a free, unlimited license to use them. Boost Your BIM will retain ownership of the app and its source code, but maybe I will share the source with the winners if I am feeling generous.

For more info on the lift family shown in this video and other amazing Revit projects byĀ Marcello Sgambelluri, visitĀ http://therevitcomplex.blogspot.com/

Dynamic Model Update to warn or log after a user operation

Maybe the approach in the previous post, of completely shutting down the Import command, is too harsh. This example shows how a Dynamic Model Update macro can give a warning after the user imports a CAD file. For some introductory comments on Dynamic Model Update, seeĀ https://boostyourbim.wordpress.com/2012/12/17/automatically-run-api-code-when-your-model-changes.

Instead of (or in addition to) the TaskDialog warning used in the code below, you could do other things like write information to a log file (to submit to Human Resources and include in the Revit user’s permanent record!) or require the user to enter a password to complete the operation – if the wrong password is entered then delete the element id returned byĀ data.GetAddedElementIds().

private void Module_Startup(object sender, EventArgs e)
{
    ImportInstanceUpdater updater = new ImportInstanceUpdater(this.Application.ActiveAddInId);
    try
    {
    UpdaterRegistry.RegisterUpdater(updater);
    UpdaterRegistry.AddTrigger(updater.GetUpdaterId(),
                               new ElementClassFilter(typeof(ImportInstance)),
                               Element.GetChangeTypeElementAddition());
    }
    catch {}
}

public class ImportInstanceUpdater : IUpdater
{
    static AddInId m_appId;
    static UpdaterId m_updaterId;
    // constructor takes the AddInId for the add-in associated with this updater
    public ImportInstanceUpdater(AddInId id)
    {
        m_appId = id;
        m_updaterId = new UpdaterId(m_appId, new Guid("FBFBF6A2-4C06-42d4-97C1-D1B4EB593EFF"));
    }
    public void Execute(UpdaterData data)
    {
        Document doc = data.GetDocument();
        Autodesk.Revit.ApplicationServices.Application app = doc.Application;
        foreach (ElementId addedElemId in data.GetAddedElementIds())
        {
            ImportInstance ii = doc.GetElement(addedElemId) as ImportInstance;
            if (ii.IsLinked == false)
                TaskDialog.Show("Hey!", app.Username + " - Maybe should should have linked that CAD instead of importing it.");
        }
    }
    public string GetAdditionalInformation(){return "Check to see if CAD file was imported";}
    public ChangePriority GetChangePriority(){return ChangePriority.FloorsRoofsStructuralWalls;}
     public UpdaterId GetUpdaterId(){return m_updaterId;}
    public string GetUpdaterName(){return "Import Check";}
}

How to override commands in the Revit UI (when you REALLY don’t want people to import CAD files)

The previous post showed how to delete CAD imports that are already in your Revit model. What if you want to prevent those CAD imports from being added in the first place?

The 2013 API introduced functionality that allows commands in the Revit UI to be replaced with your API code. In this simple case, the Import command will be replaced with an error dialog.

You will need to find the ID for the command that you want to replace. The easiest way to do that is to execute the command through the Revit UI and then open the Revit journal file (inĀ C:\Users\HP002\AppData\Local\Autodesk\Revit\Autodesk Revit Architecture 2013\Journals or something like that). For import, the entry will be

Ā Jrn.Command "Internal" , "Import vector data from other programs , ID_FILE_IMPORT"

The string in capital letters near the end of the line is the command id. Here’s the code showing how to replace the Import command, and in the video you see that while Link still works as usual, Import does not.

private void Module_Startup(object sender, EventArgs e)
{
    UIApplication uiapp = new UIApplication(this.Application);

    // Get the command id for the import command
    RevitCommandId commandId = RevitCommandId.LookupCommandId("ID_FILE_IMPORT");

    // use try/catch because CreateAddInCommandBinding will throw if there is already a binding for this id
    // for more info see https://boostyourbim.wordpress.com/2013/01/06/using-module_startup-to-run-macro-code-when-revit-starts/
    try
    {
        AddInCommandBinding importBinding = uiapp.CreateAddInCommandBinding(commandId);
        importBinding.Executed += new EventHandler<Autodesk.Revit.UI.Events.ExecutedEventArgs>(importReplacement);
    }
    catch
    {}
}

private void importReplacement(object sender, Autodesk.Revit.UI.Events.ExecutedEventArgs arg)
{
    TaskDialog.Show("Stop!","Do not import!");
}

Delete Imported DWGs and Other CAD files

In yesterday’s post we learned that imports belong to theĀ ImportInstance Class and that theĀ ImportInstance.IsLinked property is true if the element is linked and false if it is imported.

With this knowledge, we just need a FilteredElementCollector, a bit of LINQ, a transaction, and Document.Delete to get the imports (but not the links) out of the model.

public void deleteImportsNotLinks()
{
    Document doc = this.ActiveUIDocument.Document;
    using (Transaction t = new Transaction(doc,"Delete Imports"))
       {
        t.Start();
        foreach (ImportInstance ii in new FilteredElementCollector(doc).OfClass(typeof(ImportInstance)).Cast<ImportInstance>().Where(i => i.IsLinked == false))
        {
            doc.Delete(ii);
        }      
        t.Commit();
   }
}