DynaWorks

During my researches for improving clash detection, I stumble upon DynaWorks, a great Dynamo package built by Adam Sheather (@Gytaco).

For those who haven’t heard about Dynamo (Is there is any left?), it is a visual programming interface for Revit, pretty much like Grasshopper. Just like Grasshopper, you can improve upon the built-in features with packages from third party developers. DynaWorks is one of these packages, and provides a set of Dynamo nodes for interacting with Navisworks.

After installing it through the package manager, it presents itself with a set of nodes exposing Navisworks main functions.

Along with the package, some examples are provided. The NavisClashesElementUpdate.dyn contains a definition for retrieving clash results from Navisworks, I use it for my first steps with DynaWorks. If, like me, you want to use these definitions with Navisworks 2016, don’t forget to use Adam Sheather’s trick for updating your Dynamo definitions

To use DynaWorks, you first have to create a Navisworks file, and prepare your clash detections. Here, I create a basic detection between Walls (First Clash Item, in Blue) and HVAC elements (Second Clash Item, in Red), and load the resulting NWF file in my Dynaworks definition.

TestSelection

Basically, this definition retrieves every clash result in every clash test, selects one side of the clash detection, gets the involved element, extracts its id and uses it to update the Comment parameter in Revit.

process

Simple, but the result is pretty impressive. Instead of painfully getting element Ids in the Navisworks report, DynaWorks retrieves them for us, and updates elements in Revit accordingly. When applying a filter in the Revit view, we get this:

result

With some tweak to this initial definition, I was able to implement some king of dynamic clash detection, with a live update of the Navisworks file as we update the Revit model, but converting an entire Revit model in NWC is way too slow to implement this solution in a production environment. Please feel free to use it if you want to try it by yourself.

DynaWorks contains many other functions to retrieve Navisworks views or selection sets in Revit, and I think this can be the beginning of a great work flow intertwining Revit and Navisworks.

Grouping clash results

In any given building model, there is a number of issues to be addressed. A large part of these issues are what we call “hard clashes”, when a building component physically penetrate the space occupied by another building component. In these case, two or more building elements compete for the same volume.

Finding these clashes is now quite simple thanks to clash detection software that detect geometrical interferences between elements of a building model.

But after having a hard time finding these issues through coordination sections hand-drawn from plans, we now face the inverse problem and end up with too many clashes in our models. Running a simple clash detection can quickly yield thousands of clashes, making the entire process nearly useless.

Furthermore, a clash only represent a geometrical intersection between two elements, and is more a part of an issue than the issue itself. We have to group these clashes to be able to extract meaningful issues from the meaningless clashes.

Grouping these clashes is generally a manual task, and the user have to run through thousands of clashes to sort them in relevant groups.

While trying to automate this tedious task, I run across the examples provided as part of the Autodesk Navisworks Software Development Kit. These examples include a nice Clash Grouper plugin for Navisworks, enabling various method for grouping clash results.

groupClashes

As an example, I run a clash detection between the blue Selection A and the green Selection B, and get seven clashes, shown here as red dot:

Ungrouped

With the Clash Grouper, I can group these clashes by grid intersection:

groupByGrid

I can also group them by cluster analysis, where we search for the optimum grouping solution given the expected number of groups:

groupByCluster

I also add my own method for grouping clash against a specific set. I use this to group all clash belonging to a single element in one of the two selection sets.

grouplashesEdited

Here, I group by element from the Selection A (blue)

groupBySelectionA

Here, I group by elements from the selection B (green)

groupBySelectionB

If elements from one set are more relevant for the end user, the final clash report is clearer for this user when clashes are grouped against this set.

This plug-in enables a lot of possibilities for sorting clash detection results in a meaningful report, and will become a full-time member of my coordination toolbox.

To install this plug-in, you can copy-paste the ClashDetective.ADSK.dll file available here in a new ClashDetective.ADSK folder in C:\Program Files\Autodesk\Navisworks Manage 2016\Plugins. You can also see my edited version of the example code here.

Automatic Modeling

Most questions I encounter these days turn around using building information modeling for producing construction drawings, schedules or clash detection reports. These features are important, of course, but they remain centered around using a more or less complete building model. But a largely overlooked use of building information model is the ability to automatically model building elements. In fact, object-oriented modeling should allow us to simply describe our solutions and let the computer implement them in our building model, and even warn us when something doesn’t fit.

Lead by this thought, and a problem raised by one of my colleague, I started looking again toward slab insulations. Before BIM, slab insulation was simply annotated on the drawing, and taken into account when creating a building section. Now, for coordination purpose, we have to spend large amount of time modeling everywhere a fairly simple element like insulation, without real added value.

There is nothing new with this problem, and I was already trying to find a solution for modeling insulation layers on the upper face of a series of spaces with my latest version of RoomFinishes, without finding a decent solution.

But while looking for something totally different, I came across this article in AUGI about room and spaces in Revit. So after five years working with Revit, I realized than by checking this small checkbox in the “Area and Volume Computations”, the room continues up until it fit a room bounding element, a ceiling or a floor.

VolumeComputation

Energized by this nice discovery, I created a small application for automatically modeling insulation. This application retrieves every faces of a given room, and uses the higher ones as references for modeling floors and walls as insulations.

Insulation

The only problem here is framing. They are not room bounding elements, and the upper face of the room does not fit the shape of the beam. I solved temporary this problem by creating a small floor under the beam.

beam

While doing this, I realized there is a lot of tricky cases, where insulation have to be modeled as wall or slab regarding of the configuration.

The entire code for this application can be found here. It is quite messy, but I hope it can help you anyway.

This small example illustrates one of the possibilities of working with building information models, where information can actually help us focus more on the actual design of the building and less on the representation of this design in the model.

Insulation Tag

I am not very fond of duct and pipe insulations in Revit. If the concept is very useful, I find difficult to add and edit them. Furthermore, seeing the insulation on every duct and pipe can quickly clutters the view. We want to know how much insulation we have, but we don’t want to see it everywhere.

In a technical drawing, we generally represent insulation only on small parts of the duct, with an annotation, and then infer that the duck is covered everywhere else.

To adapt this method on Revit, I create two detail components, representing the insulation on a round and on a rectangular duct.

Annotations

This components have a “Width” instance parameter for setting their width regarding the thickness of the insulation.

Width

I also create a small piece of code to instantiate these components on selected ducts and pipes.

The idea is to pick a series of duct and pipe, read their insulation thickness, and add a detail component with the corresponding thickness. Like this, we can control where we want to see an insulation, and improve the overall legibility of the drawing.

Animation

This solution allow me to create quickly some annotations to represent the insulation without having to display it everywhere. However, this is more a workaround than a real solution. Nothing here is adaptive, and you have to restart the tool each time you edit or even move your duct.

public void InsulationTag()
		{
			UIDocument uidoc = this.ActiveUIDocument;
			Document doc = uidoc.Document;
			
			Reference r = null;
			
			//Retrive the two symbols
			FamilySymbol rectangularSymbol = null;
			FamilySymbol roundSymbol = null;
			ICollection<Element> collection = new FilteredElementCollector(doc).OfClass(typeof(FamilySymbol)).OfCategory(BuiltInCategory.OST_DetailComponents).ToElements();
			foreach (Element element in collection)
			{
				FamilySymbol current = element as FamilySymbol;
				// This NewFamilyInstance overload requires a curve based family
				if (current.Family.Name == "DuctInsulationAnnotationRectangular")
				{
					rectangularSymbol = current;
				}
				else if (current.Family.Name == "DuctInsulationAnnotationRound")
				{
					roundSymbol = current;
				}
			}
			

			
			try
			{
				while (true) {
					
					using( Transaction t = new Transaction( doc ) )
					{
						t.Start( "Annotate Insulations" );

						r = uidoc.Selection.PickObject(ObjectType.Element, new SelectionFilter(),"Pick a duct");
						
						if (r != null)
						{
							MEPCurve mepCurve = doc.GetElement(r.ElementId) as MEPCurve;
							//doc.ActiveView.GetElementOverrides
							//Get the middle of the duct
							LocationCurve locCurve = mepCurve.Location as LocationCurve;
							XYZ insertionPoint = locCurve.Curve.Evaluate(0.5,true);
							//Get the angle
							XYZ vector = locCurve.Curve.GetEndPoint(1) - locCurve.Curve.GetEndPoint(0);
							double angle = new XYZ(1,0,0).AngleTo(vector);
							
							//Get the insulation
							ICollection<ElementId> insulationIds = InsulationLiningBase.GetInsulationIds(doc,r.ElementId);
							
							if (insulationIds.Count !=0)
							{
								InsulationLiningBase insulation = doc.GetElement(insulationIds.First()) as InsulationLiningBase;
								
								if (insulation != null)
								{
									FamilySymbol symbol = null;
									double widht;
									try {
										widht = insulation.Width;
										//Select the detail familly
										symbol = rectangularSymbol;
									} catch (Autodesk.Revit.Exceptions.InvalidOperationException) {
										Parameter outerDiameter = mepCurve.get_Parameter(BuiltInParameter.RBS_PIPE_OUTER_DIAMETER);
										if (outerDiameter != null)
										{
											widht = insulation.Thickness*2 + outerDiameter.AsDouble();
										}
										else
										{
											widht = insulation.Diameter;
										}
										
										//Select the detail familly
										symbol = roundSymbol;
									}
									
									//Create the annotation
									FamilyInstance annotation = doc.Create.NewFamilyInstance(insertionPoint,symbol,doc.ActiveView);
									
									//Change the witdh
									annotation.GetParameters("Width").First().Set(widht);
									
									//rotate the component
									annotation.Location.Rotate(Line.CreateUnbound(insertionPoint,new XYZ(0,0,1)),-angle);
								}
							}
						}

						t.Commit();
					}
				}
				
			}
			catch( Autodesk.Revit.Exceptions.OperationCanceledException )
			{
			}
		}

public class SelectionFilter : ISelectionFilter
	{
		#region ISelectionFilter Members

		public bool AllowElement(Element elem)
		{

			if (elem.Category.Id.IntegerValue == (int)BuiltInCategory.OST_DuctCurves) return true;
			if (elem.Category.Id.IntegerValue == (int)BuiltInCategory.OST_PipeCurves) return true;

			return false;
		}

		public bool AllowReference(Reference refer, XYZ pos)
		{
			if (refer.ElementReferenceType == ElementReferenceType.REFERENCE_TYPE_NONE) return false;

			if (refer.ElementReferenceType == ElementReferenceType.REFERENCE_TYPE_SURFACE) return true;
			if (refer.ElementReferenceType == ElementReferenceType.REFERENCE_TYPE_LINEAR) return true;

			return false;
		}

		#endregion
	}

LazJS for Revit

I recently receive a beta version of LazJS, a Javascript editor embedded within Revit.

Even if I more fluent with .NET and C#, I find the initiative quite interesting, especially when you have to quickly develop some small routine within Revit.

After installing it, I click on the ParamJS button to start their embedded editor.

LazJS

As an example, I concatenate various parameters of a sheet in the sheet name in order to have them when I print or export it.

I start by selecting a category in the Categories list. Here, you can select multiple categories, but I will only select “Sheets” here. When I click on “Customize”, LazJS displays every editable parameters of our sheets in the “Parameters” list. Since I want to edit the name of my sheet, I select “Sheet Name” and save.

ParamJS

I can now drag and drop parameters from the “Formulas” list to the “Editor” panel. This creates automatically the correct line of code for calling this parameter. I drag some of my sheet parameters, add some “+” to concatenate them, and hit Run. We can see the result of my concatenation just below. I hit save, and my modification are added to my model.

Run

If this is the quickest way to edit parameters, LazJS have also a fully functional editor, which allows us to create more elaborate routines.

I use it to create the same concatenation tool than in my previous example, everything came pretty easily with the auto-completion.

ScreenClip [1]

LazJS also provide a set of already existing scripts for inspiration.

Last but not least, LazJS provides functions for exporting and importing data to Excel. These functions are very easy to use, and can replace any plug-in dedicated to exporting and importing data from Excel.

Even if I will keep my habit with C#, the LazJS plug-in seems quite powerful, and far more easy to use than the Revit macro editor, since you don’t have to handle all the tedious document retrieval and the transaction handling.

The only problem I have to report is an error when I attempt to debug a Revit macro in SharpDevelop after installing LazJS. It is kind of annoying, but I am sure this problem will be addressed when they launch their final version.

Getting started programming Revit – Part 2

Last week, we started creating macros in Revit, we saw how to get the current Revit document, and retrieved every visible walls in the current view.

We will see today how to create a tag on these walls.

To tag an element in Revit, we need the Create.NewTag() function. This function is called from the current document, named here myDocument. But to work properly, this function needs a few things as inputs.

It first requires a view to place our tag: we will just use the active view, named myActiveView.

It also needs an element to place a tag on. To do so, we will select each one of our walls with a for each loop.

foreach (Element myElement in myWalls) {
    //Do something with myElement
}

It means that for every element contained in our list of wall myWalls, we will perform some action, written between the brackets.

A few options have to be set, like the category of our tag, its orientation and its leader.

Finally, it needs a location point to insert our tag. We want our tag to be placed at the center of our wall, so we will retrieve the baseline of our wall, and create a point in the middle of this baseline:

//Get our wall
Wall myWall = myElement as Wall;
//Get its location
LocationCurve myWallLocation = myWall.Location as LocationCurve;
//Get starting point
XYZ myWallStartingPoint = myWallLocation.Curve.GetEndPoint(0);
//Get ending point
XYZ myWallEndingPoint = myWallLocation.Curve.GetEndPoint(1);
//Create the middle point
XYZ myWallCenterPoint =(myWallStartingPoint + myWallEndingPoint)/2;

Know, we have everything we need to create our tag:

  IndependentTag myTag = myDocument.Create.NewTag(
                myActiveView,
                myElement,
                false,
                TagMode.TM_ADDBY_CATEGORY,
                TagOrientation.Horizontal,
                myWallCenterPoint);

To try this, we need to draw some walls, and load in our model a Wall. We hit F8 to build the macro before running it.

But if we run it, we get the following error:

Error

Its means that we are trying to modifying something inside our model without starting what is called a transaction.

Every modification of our model has to be done within a transaction, a group of modifications that can be discarded. If you remember the list of actions we can cancel in the Revit user interface, each one of them is a transaction that had to be started be before modifying anything in our model.

Transactions

So let create a transaction:

We start by defining a scope of our transaction with the keyword using. Every piece of code between the following brackets will use the transaction named tx.

using (Transaction tx = new Transaction(myDocument))
{

}

Now our transaction is created, we can start it, execute our code, and commit these modifications in our transaction:

using (Transaction tx = new Transaction(myDocument))
{
                tx.Start("Add Tags on walls");

                foreach (Element myElement in myWalls) {
                    //Do something with myElement

                    //Get our wall
                    Wall myWall = myElement as Wall;
                    //Get its location
                    LocationCurve myWallLocation =
                             myWall.Location as LocationCurve;
                    //Get starting point
                    XYZ myWallStartingPoint =
                             myWallLocation.Curve.GetEndPoint(0);
                    //Get ending point
                    XYZ myWallEndingPoint =
                             myWallLocation.Curve.GetEndPoint(1);
                    //Create the middle point
                    XYZ myWallCenterPoint =
                        (myWallStartingPoint + myWallEndingPoint)/2;

                    IndependentTag myTag = myDocument.Create.NewTag(
                             myActiveView,
                             myElement,
                             false,
                             TagMode.TM_ADDBY_CATEGORY,
                             TagOrientation.Horizontal,
                             myWallCenterPoint);
                }

                tx.Commit();
}

We run it and every walls are tagged.

Walls

Use Grasshopper to produce fabrication drawings

I recently have to produce fabrication drawings from a set of panels covering a single curved surface.

These panel were modeled in Rhino 3D, so I decide to extract their boundary curves and use AutoCAD to produce a drawing for every one of them.

I build here an example by extruding a surface between two symmetrical splines.

1_BaseSurface

This construction method create a single curved surface, which is the easiest to cover with plane panel, here with the “Quad Panels” component from the LunchBox plugin.

2_PanelSurface

Then I extract every panel edge and orient it horizontally on the base XY plane.

3_PanelEdges

My objective here is to bake every panel boundary curve on a different layer to be able to sort them separately in AutoCAD. To do so, I use the “Object Bake” component from LunchBox. I also use a “Series” to create a layer names for each panel. Here the names are only numbers formatted as text.

4_BakeEdges

The next step is to add dimensions. Sadly, since native grasshopper dimension does not have any output object, we cannot bake them with the “Object Bake”. So I’m using my own Linear Dimension creation component. As you can see, the code is quite straightforward. I use standard Rhino functions to create my dimensions, and have a small private function for selecting or creating the layer to place our dimension.

 private void RunScript(bool Bake, Point3d A,
                        Point3d B, Point3d C, 
                        string Layer, ref object O)
  {

    if (Bake == true)
    {

      Point3d A0 = new Point3d(A.X, A.Y, 0);
      Point3d B0 = new Point3d(B.X, B.Y, 0);

      double a = (B0.X - A0.X) / (B0.Y - A0.Y);
      double Xc = 1;
      double Yc = Xc / a - A0.X / a + A0.Y;

      Point3d C0 = new Point3d(C.X, C.Y, 0);

      Plane refPlane = new Plane(A0, B0, C0);

      double u,v;
      refPlane.ClosestParameter(A0, out u, out v);
      Point2d A2d = new Point2d(u, v);

      refPlane.ClosestParameter(B0, out u, out v);
      Point2d B2d = new Point2d(u, v);

      refPlane.ClosestParameter(C0, out u, out v);
      Point2d C2d = new Point2d(u, v);

      Rhino.Geometry.LinearDimension dimension = 
                  new LinearDimension(refPlane, A2d, B2d, C2d);

      ObjectAttributes dimAtt = new ObjectAttributes();
      dimAtt.LayerIndex = ensureLayer(tag);

      if (Rhino.RhinoDoc.ActiveDoc.
Objects.AddLinearDimension(dimension, dimAtt) != Guid.Empty)
      {
        doc.Views.Redraw();
      }
    }

  }

  // <Custom additional code> 
  private int ensureLayer(string lay)
  {
    int i = doc.Layers.Find(lay, true);
    if(i < 0)
      return doc.Layers.Add(lay, Color.Black);
    else
      return i;
  }

With the “Disc” component, I retrieve every summit of my panel boundary, and place a dimension between two of these points. I also use an offset of my panel boundary to set up the position of the dimension line.

5_LinearDimension

Angular dimensions follow more or less the same principle, with two shift to retrieve the point before and the point after a given submit to create our angle measure.

6_AngularDimension

These two dimension components are linked to layer names, so when hitting Bake, each panel curve end up in its layer along with its dimensions. You can see the result below, with every layer displayed and with only one layer visible.

7_Result

I can now export every curve and its dimensions to a DWG file. This file is edited with an AutoCAD script to paste each layer in a new template file where the titleblock is prepared. It also add the panel number in the titleblock.

7_FinalDrawing

Use Navisworks Batch Utility to convert Revit files

A few days ago, I had to convert a large set of Revit files to NWC in order to create a general Navisworks File Set.

I used the Navisworks Batch Utility, accessible through the Navisworks main menu :

Icon

Batch Utility

You first have to select files to be included in your Navisworks File Set.

To quickly retrieve the list of Revit files to be converted, I’m using the Windows Command Prompt. I was quite afraid of this tool not so long ago, but it is actually pretty simple.

Windows Command Prompt

First, go to the root folder of your project:

cd C:\Projects\myRevitProject

and type :

dir /s /b *.rvt &amp;amp;amp;amp;amp;gt;ListRevitFiles.txt

This line requires a bit of an explanation:

  • dir : command for searching and displaying file in the current directory
  • /s : search in the current directory and all its subdirectories
  • /b (or bare) : remove for each file its metadata to display only the file path
  • *.rvt : search specifically for Revit files
  • >ListRevitFiles.txt : the ‘>’ character allows us to output the result of our research to a text file (here ListRevitFiles.txt) instead of displaying it.

We get the results of our research as text file listing paths to every Revit model contained in our project folder:

C:\Projects\myRevitProject\CENTRALS\ARC.rvt
C:\Projects\myRevitProject\CENTRALS\MEP.rvt
C:\Projects\myRevitProject\CENTRALS\STR.rvt

Back on the Navisworks Batch Utility, we open this text file to import file paths: File -> Open -> Select ListRevitFiles.txt

File List

As we want to create a single Navisworks File Set (.nwf), we select the “As Single File” Tab, and set the path to our future Navisworks File.

As Single File

I also select “View file on output” to automatically start Navisworks when conversions are done.

We add a path to a log file in order to know what may happen, and hit “Run Command”.

Here, I was confused by the fact that nothing seems to happen, but after checking my computer processes, I was able to see that the Navisworks Scene Convert Server was up and running.

Windows Processes

After a while, Navisworks starts automatically and appends every previously created .nwc file to a new Navisworks File Set.

Project

I am also using this feature to create a NWD file for a set of Revit file.

To do so, you just have to select the “Multiple file” tab and define a target folder for the export.

As Multiple Files

The Navisworks Batch Utility will convert every Revit file to a NWC cache file, and made it a NWD on the run.

 

Room Finishes Update

A new version of my Revit plug-in Room Finishes is available on the Autodesk App Exchange.

This major update integrate a new feature for creating floor finishes. The main idea is to create a floor that follow the general outline of a room, at a given height offset from the room level.

The first application is to model quickly floor finishes inside every selected room. Just select a floor type, a height offset and the plug-in will model a finish floor on every selected room.

CreateInterface

We can see here the floor created with the previous parameters:

ResultDetail

The whole idea came when I have to model an insulation just under the slab for more than a thousand of rooms. Luckily, these rooms where correctly modeled, with their upper limit set just below the upper slab.

RoomHeight

So I wrote a small piece of code for creating a floor with the same boundaries than the selected rooms. Just like my previous plug-in for creating skirting board, you just have to select a type of floor to create, enter the desired height and the application will create the floor in the selected rooms.

In my problem, the distance between the room level and the upper slab could change, so I introduce an additional feature, the ability to select one of the parameter of the room to define the height of the floor.

ParamSelector

Since my rooms are modeled from slab to slab, I just have to select the Unbounded Height room parameter to create insulation at the correct elevation.

InsultationResult

I am also using it to draw a temporary floor at the ceilling height in every room of a project. This temporary floor is used to run a clash detection to check if every HVAC objects are correctly placed above the drop-ceiling height.

The application is already available in the Autodesk App Exchange. If you already have installed Room Finishes, just download the update from the same link.

I also finally clean up my code and upload it on GitHub for everyone to see. The source code is freely available here as a Visual Studio 2012 solution. Feel free to download and use it for any of your own application. I would be delighted if you can use it for something useful.

Managing linked worksets

A pretty powerful function in Revit is the ability to manage worksets in a linked Revit file.

LinkedFiles

In the Manage Link window, selecting the Manage Worksets open a list of user worksets in the linked file. These linked worksets can be opened or closed through the same interface.

LinkedWorkset

It is a project setting, so elements in the closed workset will be hidden everywhere in our host model.

It allow us to load only the part of the linked project that really interesting us. For example, as a mechanical engineer, I generally don’t need to display furniture from the architectural model. Closing the related workset can save me significant loading time.

The most common application of this feature is to hide linked grids and references planes.When new workset are created, every level and grid goes into the “Shared Levels and Grids” default workset. This behavior should not be changed, since it allows us to easily hide linked levels and grids in our current model.

Before closing linked "Shared Levels and Grids" workset

Before closing linked “Shared Levels and Grids” workset

Before closing linked "Shared Levels and Grids" workset

After closing linked “Shared Levels and Grids” workset

I also place reference planes and scope boxes in this workset, to be able to hide them as easily. But selecting all reference planes and scope boxes of a model to place them in the correct workset can a tedious business. To change this, I wrote a few line of code for set up every Scope Box, and Reference plane to the correct workset :

public void SetGridWorkset()
{
	Document doc = this.ActiveUIDocument.Document;

	//Select the shared grid workset
	IList<Workset> worksetList =
		new FilteredWorksetCollector(doc)
			.OfKind(WorksetKind.UserWorkset)
				.ToWorksets();
	int sharedGridWorksetId=0;

	foreach (Workset workset in worksetList) {
		if (workset.Name.Contains("Shared Levels and Grids"))
		{
			sharedGridWorksetId = workset.Id.IntegerValue;
		}
	}

	if( sharedGridWorksetId == 0 ) return;

	//Reference planes
	List<Element> elements =
		new FilteredElementCollector(doc)
			.OfCategory(BuiltInCategory.OST_CLines)
				.ToElements().ToList();
	//Scope box
	List<Element> scopeBoxes =
		new FilteredElementCollector(doc)
			.OfCategory(BuiltInCategory.OST_VolumeOfInterest)
				.ToElements().ToList();
	elements.AddRange(scopeBoxes);

	using (Transaction tx = new Transaction(doc)) {
		tx.Start("Change Workset");

		foreach (Element e in elements) {

			//Retrive workset parameter
			Parameter wsparam =
				e.get_Parameter(BuiltInParameter.ELEM_PARTITION_PARAM );
			if( wsparam == null ) continue;

			//set workset to Shared Levels and Grids
			wsparam.Set(sharedGridWorksetId);
		}

		tx.Commit();
	}
}

This piece of code is inspired from the work of Jeremy Tamick, available here, thanks to him.