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.
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.
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.
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:
In the project environment when they attempt to open the Family Types dialog
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)
Yesterday’s post was about getting Family Parameter data out of Revit and into Excel. Here you can see:
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
Data modified in Excel for multiple families
Data read back into the family files
Modified families saved to disk
Families manually opened to show the changes made with the API commands
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?
publicvoid 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 directoryforeach (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 familyforeach (string paramName in paramSorted)
{
FamilyParameter familyParameter = familyManager.get_Parameter(paramName);
s += familyParameter.Definition.Name + ",";
}
s += "\n";
// create a row for each family typeforeach (FamilyType familyType in familyManager.Types)
{
if (familyType.Name == " ") // skip the 'default' family type with no namecontinue;
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 diskstring 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);
}
privatestring ParameterValueForType(FamilyType type, FamilyParameter p)
{
// return a string with the value of the parameter for this typestring 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;
}
To 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.
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?
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.
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?
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.
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.
Should there be password restrictions on both opening existing families and creating new families?
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.
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.
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).>
publicvoid 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.
publicvoid 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);
}
}
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
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:
Integer parameters are now supported. This allows you to modify array parameters in a family.
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.
A friend asked about a macro to renumberĀ grids in the order they are picked. Here’s how:
publicvoid 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 elementwhile (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 loopcatch
{}
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 oneint 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 loopif (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");
elseif (e is Room)
p = e.get_Parameter("Number");
elseif (e is FamilyInstance)
p = e.get_Parameter("Mark");
else
{
TaskDialog.Show("Error","Unsupported element");
returnnull;
}
return p;
}
privatevoid setParameterToValue(Parameter p, int i)
{
if (p.StorageType == StorageType.Integer)
p.Set(i);
elseif (p.StorageType == StorageType.String)
p.Set(i.ToString());
}
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.
publicvoid 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 filestring 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 timeusing (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 stringif (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-conceptusing (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();
}
}
privatestring listToString(IList<string> stringList)
{
string ret = "";
foreach (string s in stringList)
{
ret += s.Replace("\t"," ") + "\n";
}
return ret;
}
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/
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().
privatevoid 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 {}
}
publicclass ImportInstanceUpdater : IUpdater
{
static AddInId m_appId;
static UpdaterId m_updaterId;
// constructor takes the AddInId for the add-in associated with this updaterpublic ImportInstanceUpdater(AddInId id)
{
m_appId = id;
m_updaterId = new UpdaterId(m_appId, new Guid("FBFBF6A2-4C06-42d4-97C1-D1B4EB593EFF"));
}
publicvoid 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.");
}
}
publicstring GetAdditionalInformation(){return"Check to see if CAD file was imported";}
public ChangePriority GetChangePriority(){return ChangePriority.FloorsRoofsStructuralWalls;}
public UpdaterId GetUpdaterId(){return m_updaterId;}
publicstring GetUpdaterName(){return"Import Check";}
}
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.
privatevoid 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
{}
}
privatevoid importReplacement(object sender, Autodesk.Revit.UI.Events.ExecutedEventArgs arg)
{
TaskDialog.Show("Stop!","Do not import!");
}
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.
publicvoid deleteImportsNotLinks()
{
Document doc = this.ActiveUIDocument.Document;
using (Transaction t = new Transaction(doc,"Delete Imports"))
{
t.Start();
foreach (ImportInstance ii innew FilteredElementCollector(doc).OfClass(typeof(ImportInstance)).Cast<ImportInstance>().Where(i => i.IsLinked == false))
{
doc.Delete(ii);
}
t.Commit();
}
}