api

From Dynamo to C# – Get and Set Parameters

I’m the kind of guy that uses Dynamo for practical things, an indispensable swiss army knife that I used to manipulate data.

Getting and setting parameters is obviously key. This time around we look at how this is done in Dynamo and then how we can achieve that same thing with code.

In this example I will look at how to build up an asset code based on the level name and the mark parameter, combining those into a single new parameter value.

We will start with the code from the previous Dynamo to C# post where we collected all the AHU families in the model. For convenience, here is the complete code from the previous post:

public void getMechanicalEquipment()
{
	//get current document
	Document doc = this.ActiveUIDocument.Document;
	
	//setup collector and filter
	var collector = new FilteredElementCollector(doc);
	var filter = new ElementCategoryFilter(BuiltInCategory.OST_MechanicalEquipment);

	//collect all mechanical equipment
	var allMechanicalEquipment = collector.WherePasses(filter).WhereElementIsNotElementType().Cast<FamilyInstance>();

	//use LINQ to filter for familyInstances containing AHU in the family name
	var ahuEquipment = from fi in allMechanicalEquipment
		where fi.Symbol.FamilyName.ToUpper().Contains("AHU")
		select fi;

	//setup string builder to store data
	StringBuilder displayInfo = new StringBuilder();

	//loop through, collect data
	foreach (FamilyInstance fi in ahuEquipment)
		{
			var elementinfo = string.Format("Family Name: {0} Element Id: {1}", fi.Name, fi.Id);
			displayInfo.AppendLine(elementinfo);
		}

	//display results
	TaskDialog.Show("Watch", displayInfo.ToString());
}

Getting the Level

To do this in Dynamo, you would need to string together the following nodes:

In Dynamo we need to use the Level.Name node to get the actual name. The reason for this is that when we feed Level into the Element.GetParameterByName node, the output is actually an element id. Feeding that element id into the Level.Name node returns the name without any further work required.

So how do we replicate this in C#?

First, we need to focus on the foreach loop, as this is where the work is done for each element that we have selected. We have already done the groundwork to select our elements, but how do we find out what level each element is associated with?

We need to use the level class from the API, this allows us to retrieve the name of the level as you would see it when inspecting the element you’re working with.

Using the level class, we create a variable theLevel which we find from the family instance fi.

Level theLevel = fi.Document.GetElement(fi.LevelId) as Level;

This line is doing half the work of our Level.Name Dynamo node. It is taking the element id of the level that is reported from the Level parameter of the selected AHU family, then selecting the level element.

From there we can simply use theLevel.Name to display the level name.

//loop through, collect data
foreach (FamilyInstance fi in ahuEquipment)
{
	Level theLevel = fi.Document.GetElement(fi.LevelId) as Level;
	var elementinfo = string.Format("Family Name: {0} Level: {1}", fi.Name, theLevel.Name);
	displayInfo.AppendLine(elementinfo);
}

Note that instead of using the elementinfo variable to display the family name element id of the AHU in the dialogue box as per the previous example, we are now displaying the family name and the associated level name.

Getting the Mark

There isn’t a whole lot of change between the process for picking up the mark parameter in Dynamo compared to the level name; the only real change is that we can drop the Level.Name node.

This is because when we tell the Element.GetParameterValueByName node that we want a parameter named Mark, it returns the result as a string.

In C# we want to use get_Parameter on the family instance fi.

But before we get too far, we need to understand that built in parameters are handled slightly differently in Revit to user defined parameters. Built in parameters are contained within an enumerated list, you can review the list of built in parameters on Revit API Docs.

In the API, the mark parameter is stored as ALL_MODEL_MARK

Retrieving built in parameters this way is more reliable as there is no risk that you will pick up a user defined parameter of the same name.

Now that we know this, our equivalent line of code to read the mark parameter in C# looks like this:

Parameter theMark = fi.get_Parameter(BuiltInParameter.ALL_MODEL_MARK);

But we still don’t have the parameter value. To retrieve the parameter value, we need to use the AsString method on our paramter theMark which will return the parameter value as a string.

It is important to use the correct method to avoid any potential errors returning the data, for numbers use AsDouble, for integers use AsInteger and element ids use AsElementId.

Our foreach loop should now look like so:

//loop through, collect data
foreach (FamilyInstance fi in ahuEquipment)
{
	//get the level
	Level theLevel = fi.Document.GetElement(fi.LevelId) as Level;
	//get the mark
	Parameter theMark = fi.get_Parameter(BuiltInParameter.ALL_MODEL_MARK);

	var elementinfo = string.Format("Family Name: {0} Level: {1}", fi.Name, theLevel.Name);
	displayInfo.AppendLine(elementinfo);
}

Again, adding this to our task dialogue, we end up with the following results.

Building the New String

We can build our new parameter value a few different ways in Dynamo, either by using the String.Concat node, or by using a code block which is my preferred method.

There are also a few different way you can approach building the new string in C# as well. The simplest way is to approach it the same way as we do in the Dynamo code block.

First you define the string, in this example we are naming it newValue and we take our level name theLevel.Name, add the string _AHU_ and the we take our mark value with theMark.AsString().

string newValue = theLevel.Name + "_AHU_" + theMark.AsString();

The other method is to use a string builder which we have been using in our examples to create our dialogue box message. The string builder version would look like so:

var newValue = string.Format("{0}_AHU_{1}", theLevel.Name, theMark.AsString());

Either method is acceptable, but if you’re building more complex strings the string builder is the recommended way to go. Both options give the exact same results:

Setting the New Parameter

In Dynamo, the job here is done by the trusty Element.SetParameterByName node seen above, but in C# there is a tiny bit more to do.

The first thing we need to do to set out parameter is define which parameter we want to set. In this example I’m going to use the built in parameter Comments which is stored in Revit as
ALL_MODEL_INSTANCE_COMMENTS.

Parameter theComments = fi.get_Parameter(BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS);

The next step is to start a transaction. Every time you want to change something in a Revit file via the API, you need to make the changes inside a transaction. If you have ever looked into Dynamo nodes containing Python code, you would have seen something similar to this before.

//set the parameter
using(Transaction t = new Transaction(doc, "Set Parameter"))
{
	t.Start();
	//do stuff here
	t.Commit();
}

We define a new transaction t which is being performed in the current document doc which will appear in the undo/redo list as Set Parameter.

Inside our transaction, we set the parameter by using the Set method on our parameter theComments.

//set the parameter
using(Transaction t = new Transaction(doc, "Set Parameter"))
{
	t.Start();
	
	theComments.Set(newValue);
	
	t.Commit();
}

And that’s it! We have successfully pulled information from multiple parameters, generated new string data and set another parameter using C#.

The complete code is below:

public void getsetParameters()
{
//get current document
Document doc = this.ActiveUIDocument.Document;

//setup collector and filter
var collector = new FilteredElementCollector(doc);
var filter = new ElementCategoryFilter(BuiltInCategory.OST_MechanicalEquipment);

//collect all mechanical equipment
var allMechanicalEquipment = collector.WherePasses(filter).WhereElementIsNotElementType().Cast<FamilyInstance>();

//use LINQ to filter for familyInstances containing AHU in the family name
var ahuEquipment = from fi in allMechanicalEquipment
	where fi.Symbol.FamilyName.ToUpper().Contains("AHU")
	select fi;

//setup string builder to store data
StringBuilder displayInfo = new StringBuilder();

//loop through, collect data
foreach (FamilyInstance fi in ahuEquipment)
	{

		//get the level
		Level theLevel = fi.Document.GetElement(fi.LevelId) as Level;

		//get the mark
		Parameter theMark = fi.get_Parameter(BuiltInParameter.ALL_MODEL_MARK);

		//get the comments
		Parameter theComments = fi.get_Parameter(BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS);

		var newValue = string.Format("{0}_AHU_{1}", theLevel.Name, theMark.AsString());

		var elementinfo = string.Format(newValue);
		displayInfo.AppendLine(elementinfo);

		//set the parameter
		using(Transaction t = new Transaction(doc, "Set Parameter"))
		{
			t.Start();

			theComments.Set(newValue.ToString());

			t.Commit();
		}

	}

}

Revit Bulk Unit Conversion Addin

One of the most downloaded files here at Revit.AU is the metric conversion journal script. Despite the obvious downsides of journal scripting, it works about 98.27% of the time.

Based on feedback, the problems with the current journal conversion script are mostly due to user error, but occasionally even when the user followed the steps correctly, a problem would crop up that would stop the process in it’s tracks.

Because of this, one of my goals when learning how to use the Revit API has been to replace this journal script with a fully fledged addin. In my opinion, the biggest benefits seen by developing an addin to perform unit conversions are:

  • User-friendly interface
  • Ability to convert to both metric and imperial in the same tool
  • Automatically closes most popup dialogues
  • Handles sub-folders of files without issue
  • Much, much faster

Benchmarking

If I’m honest, I regret my choice of benchmarking method, a much smaller dataset would have done the job.

I rounded up 500 families totaling 169mb and ranging in size from 248kb to 1.5mb comprising of both 2D annotation and detail items as well as 3D components from architecture and MEP disciplines. I ran the same set of families through both the journal file conversion method and the addin conversion method, recording the total elapsed time for each.

I also used two different hardware configurations were in the benchmark tests; an i5 8400H with 32gb RAM and a 512gb NVME SSD and a i7 7820HK with 64gb RAM and a 1tb NVME SSD. The tests were run on Revit 2018.3 on both machines.

I hit the go button and decided to get some breakfast. This is going to take a while.

2 hours and 3 minutes later on the i5 8400H, the journal script failed on family 366. Repeating the same test on the i7 7820HK reached the failure point in around 1 hour and 40 minutes.

The addin, on the other hand, churned through all 500 families without error in 14 minutes on the i5 and 12 minutes on the i7.

Call it commitment or call it stupidity, but I also decided to time how long it would take to change the content in a single family manually to really showcase the power of automation, even if it’s on a tiny scale.

Manually updating a single family to change everything that the addin changes took 7 minutes and 18 seconds.

Of course, you normally wouldn’t change everything that the addin does, but we all love an apples to apples comparison. If you’re feeling like you need some extra excitement in your life, based on 7 minutes and 18 seconds to change a single family, manually updating 500 families will keep you busy for almost 61 hours!

Reliability

So far, I have processed close to 5000 different families from a range of different sources across all versions of Revit from 2017 to 2020 without a single error.

And let’s face it, although 365 of 500 families were processed with the journal, I couldn’t complete my automation without an error.

Couple the reliability of the addin with its ease of use and as an end user, you should have a vastly improved experience compared to the journal script.

Download

So if converting files between metric and imperial is something that you do, you can download the addin for free here:

Think this tool is worth something? Consider donating to support the operation of the site and the development of further tools in the future.

Removing Revit Line Patterns with C# Macros

Sometimes you might encounter an element within Revit giving you grief.

Recently for me it was a line pattern that had been transferred across from an old template. I didn’t want to spend the time to re-create all the old line patterns in a new template, but that time ended up being lost troubleshooting a fatal error.

Lucky for me that the line patterns were named so inconsistently in the old template or I wouldn’t have even discovered the problem; an unexpected benefit to others not being as meticulous as I can be I suppose.

One by one, I check each line pattern I had imported and discovered there was just one causing the problem. I couldn’t change the pattern definition. I couldn’t rename it. I couldn’t delete it. No matter what I did, Revit would crash.

An audit? No. What about a purge? Still no love.

So what do you do in this situation? I ended up turning to the API to obliterate the pesky line pattern. Dynamo is great but you can make a fantastic toolset based around C# macros and it’s a great way to learn the basic of coding with the API.

public void DeleteLinePattern()
{
//Get the current document
	
UIDocument uidoc = this.ActiveUIDocument;
Document doc = uidoc.Document;
			
/*
my problem line pattern started with a certain prefix, 
so the method i am using is to search for line patterns with that prefix
update your code to prefix that you're looking for
*/
		
var collector = new FilteredElementCollector(doc)
	.OfClass(typeof(LinePatternElement))
	.Where(i => i.Name.StartsWith("PREFIX")).ToList();
			
	List<ElementId> ids = new List<ElementId>();

//Start the transaction that will modify your document
			
	using(Transaction t = new Transaction(doc,"Delete LinePatterns"))
		{
		t.Start();
			
		try
		{
			foreach (ElementId id in ids)
			{
				doc.Delete(id);
			}
		}
		catch (Exception)
				
	t.Commit();
	TaskDialog.Show("Delete LinePatterns","Deletion complete");
	}
}

I’m still waiting to hear back from Autodesk as to if I am still at risk of the model becoming corrupt in the future, but in the current state I’m pretty happy as I can continue working without issue.

As you can probably tell, this is quite a simple macro and the API is capable of doing much more. If you’re interested in learning the Revit API, check out these resources on the web

Harry Mattson’s Boost Your BIM
https://boostyourbim.wordpress.com/

Harry’s Udemy Courses
https://www.udemy.com/revitapi/
https://www.udemy.com/revitapi2/
https://www.udemy.com/revit-api-materials/

Danny Bentley’s Channel on Youtube
https://www.youtube.com/channel/UC1Dx-jGyRbvvHzZ8ZyGWF5w

Jeremy Tammik’s Building Coder
https://thebuildingcoder.typepad.com/

Revit API Docs Online
http://www.revitapidocs.com/

Autodesk “My First Revit Plugin”
https://knowledge.autodesk.com/support/revit-products/learn-explore/caas/simplecontent/content/my-first-revit-plug-overview.html

Free C# Courses
https://www.learncs.org/
https://www.sololearn.com/Course/CSharp/

ItzAdam5X on Youtube for learning general C# concepts
https://www.youtube.com/channel/UC9pq4hre8qZI132O4cok5vA