Module 9

[Duplicate Views by Sheet]

A few weeks ago I had an annoying problem in Revit where I wanted to replicate a sheet, exactly as it was, several times so I could annotate the views differently on each sheet. One way of solving this might have been to create a grid that lets you place views in the same position on different sheets however, I decided to go the more exciting route of using Dynamo to solve this problem as it was faster and more efficient.

The script below is the result; it uses a technique that I commonly use to select sheets by their name or number and then copies those sheets with their views, schedules and annotation, all at once. I found this to be quite handy when I needed to copy sheets exactly, very quickly.

One thing to note is that you may experience problems if you try and rerun the script straight away. This is because as you run the script once, you are creating sheets and therefore rerunning immediately will cause duplicate sheets warnings. Therefore, make sure to change the sheet numbers before you rerun, or delete the duplicates you already created and reset the script using the boolean toggle.

Hopefully you learn some new Python while reading/using this module and as always, if you come across anything you don’t understand, please don’t hesitate to drop me an email. The script is downloadable at the bottom of the page.


To start the script, we use a technique that I commonly use to filter out the sheets in the project file. First a Boolean node is used as a script toggle, this can be turned on and off to run the script. This is then fed into a Python Script which will gather the project sheets. This is described in more detail in part 1.1 below.


The output of the Python Script in part 1 is a list of 3 sublists which contain the sheet elements, sheet names and sheet numbers respectively. We can therefore use a List.GetItemAtIndex node to retrieve these sublists individually. The top node of this type retrieves the Sheets as elements and the bottom node retrieves the sheet names using the integers 0 and  1 in the Code Block node.

learn dynamo 3 three

Here we can select which sheets we actually want to copy. With the list of sheet names coming out of part 2, we can use a SelectItem.ByList node from the Juggernaught Package. This node isn’t essential, I only made it as it makes tasks like this easier for myself. Alternatively you could use a List.GetItemAtIndex node and use integers to select the sheet names you want.

Anyhoo, after select the sheet names we want, we can then use this output for the item parameter in a List.AllIndicesOf node and the original list of sheet names for the list parameter to retrieve all indices at which the selected sheet name occurs. This output is then flattened using a Flatten node and fed into another List.GetItemAtIndex node. This is used to retrieve all of the sheets we want to copy as elements from the sheets element lists obtained in part 2.

learn dynamo module 4

With the sheets we want to copy selected in part 3, here we can copy them to create the new sheets of which we will place the copied views and such. The list of sheet elements are plugged straight into another Python Script node which is used to essentially duplicate the sheet. This is described in more detail below in part 4.1. The inputs for this Python Script are two lists which are created using the List.Create nodes. These two lists are created to input the sheet numbers and sheet names that you want to use from your new sheets. As you can see I’ve used the sheet numers A202 and A203 with the sheet names LearnDynamo Sheet 1 and LearnDynamo Sheet 2. Make sure the number of sheet numbers and names correlate to the number of sheets you want to copy otherwise Revit will taking the naming convention into its own hands. The output of the Python Script should be the newly created sheet duplicate with no views, schedules or annotation just yet.


Now that we have some fancy new sheets, we can begin duplicating the views and such that were on the original sheets and placing them accordingly. The first we want to copy over are the views which we do here using a Python Script. The first input of this node is a Boolean node which has been renamed to Copy Views On/Off which allows us to turn this feature on and off.  Secondly the originally selected sheet elements are input to IN[1] along with their duplicates to IN[2]. The last input to IN[3] is another Boolean node renamed to Copy Detailing On/Off which allows us to specify whether we want view detailing copied along with the view. The details of the inner workings of the Python Script are demonstrated in part 5.1 below.


Here we can specify whether or not we want to copy across the schedules along with the duplicated sheets. The inputs are similar to part 5 however, the Boolean node has been renamed to Copy Schedules On/Off which does exactly as it sounds; specifies whether to copy the schedules or not. The details of this Python Script are shown below in part 6.1.


Finally, here we can choose whether to copy across any detailing on the sheets in the form of lines or text. As with part 5 and 6, this feature can be turned on or off using the renamed Boolean node. The details of this Python Script node are explained in detail in part 7.1.


As always, the relevant modules are imported to use in Python. I’m not going to go into to much detail here but if you need help with module imports, please let me know. The current document is set to the common doc variable.


The boolean node input is set to the toggle variable which we can later use to turn the script on and off. A series of empty lists are then created which will also later be used to store data.


If the toggle is set to True in part 1, the script can then run.  Firstly we collect all the sheets in the project using a FilteredElementCollector on the document which we earlier set to doc variable. Using the Element Collector, we call the OfCategory method using BuiltInCategory for sheets and class ViewSheet which will collect all sheets in the project.


With all the sheets in the project stored in the collector list, we can cycle through the list to get the information we want. A for loop is used to do this and with each loop we append the sheet element, sheet name and sheet number to the relevant lists that we created in part 1.2. This is done using the get_Parameter method with  BuiltInParamater.SHEET_NAME and SHEET_NUMBER as the parameters which retrieves the underlying properties of the sheets. These are then converted to strings using AsString(). Once the loop has cycled through each sheet in the project, all the lists are appended to the out list so we can later choose which of these lists we want to access.


If the toggle in part 1 is set to False, a string is appended to the out list to remind the user that the script is not running. Finally we set the out list as the script output.


This Python Script is used to copy the original sheets so we can later copy the views, schedules and detail items to them. It starts by setting the inputs to the variables sheets, numbers and names and creating the empty list out. We then start a for loop to cycle through each of these inputs simultaneously using the zip function. This means that each loop through will be using the same index of each of these lists.


In each loop we start by setting the variable titleB  to None. This is so we can later set an object to the variable and if not, the variable can still be used as None as i will explain later. Next we create a FilteredElementCollector using the document (doc) as the parameter and the function OwnedByView with the paramater ElementId(Sheet.Id). This basically means we are searching the sheet element for all the elements that are on the sheet. The reason we have cast Sheet.Id as an ElementId is because the sheet has not been unwrapped from Dynamo to be used with the Revit API.


Once we have gathered all the elements on the sheet in the loop, we then need to start another loop to cycle to find the Title Block that the sheet is using. So, another for loop is created to cycle through each element (ele) in the list of elements (collector). For each of these elements, we try to check if the Category.Name property of the element is equal to Title Blocks. If it is, we then set the Type of the Title bock to the TitleB variable using GetTypeId function. If the element is not of the title block type then it is passed. And, if any of the elements do not have the Category.Name property, these are also passed as we have the except statement to catch these.


Next we need to create the new sheet. We do this by first starting a Transaction in Revit by using the EnsureInTransaction function with the document (doc) as the parameter. We then create a new sheet using the ViewSheet class and the Create Method. The parameters for this method are the document (doc) and the title block (TitleB) which, if the original sheet does not have, will be set to None. Once the sheet is created, the transaction ends using the TransactionTaskDone.


After the sheet has been created, we can then adjust its properties to what we want. The SheetNumber property is set to the number variable in the current loop, as well as the Name property. The new sheet (newSheet) is then appended to the out list which is the script output (OUT).


This script is used to duplicate the views from the original sheet to the new sheet. The inputs for this script are the boolean switch (toggle), the original sheets (sheets) and the detailing boolean switch (detailing). The usual out list is also created. 


First we check if the toggle is True and if so, a for loop is created to simultaneously cycle through the sheets and nsheets using the zip function. In each loop, a views list is created which will store all the newly created views and all the view ports are gathered from the original sheet using the GetAllViewPorts function.


Once we have all the view ports, we can start gearing up to duplicate their views by starting a transaction in Revit using EnsureInTransaction. Then we create another for loop to cycle through each of the viewports. As all the view ports are currently stored as element Ids in the vPorts list, we first get the view port as a viewport element by using the GetElement function with the id (ele) as the input. 

We then check whether the detailing toggle is true and if so, create a WithDetailing view options type and set it to the vOpt variable. If the toggle is False, we set the Duplicate option to vOpt.


Before we duplicate the view, we need a little bit more info such as the centre of the viewport which is obtained using the GetBoxCenter function which is stored in the viewLoc variable. This will later be used to specify the location of the new view ports. We also need the view port type which is retrieved using the GetTypeId function.

Next we need the actual view that the view port is referencing which we can get by using the GetElement function on the document (doc) with the viewport ViewId property as the input parameter. Once we have this view (view), we can then duplicate it by simply using the Duplicate method on the view with the view options (vOpt) that we created earlier as the parameter.


We can now create a new view port on our new sheets with all the information we’ve gathered. To do this, we use the Viewport class function Create using the document(doc), new sheet (nSheet.Id), view (v) and the new view location (viewLoc) which we retrieved earlier. With the new view port in hand, we can append it to our views list and change the view port type to the type of the original using the ChangeTypeId function.


Once the second loop has finished we end the Transaction using TransactionTaskDone. Once all the sheets have been looped through, we append the list of views to the output list (out). If the toggle was false, a string it output.


This script is used to copy across schedules to the new sheets. I’m not going into detail with the inputs as it’s much the same as part 5.1. So, if the toggle is true, we create a loop through the original and new sheets much like part 5.2. In this loop however, a FilteredElementCollector is used to get all the elements on the sheet using the OwnedByView function. This will be used to find all schedules on the sheet much like how the Collector was used to gather title blocks in part 4.2.


Once we have all the sheet elements, we start a Transaction in the Revit document (doc). A for loop is then started to cycle through each element on the sheet. If the element Category is None or the Category.Name is not that of a Schedule, the element is passed over. If however is is of Category.Name Schedule Graphics, we have found a schedule in the collector list and the current loop continues.


When we have a schedule, we need to check it’s not a revision schedule which is done by checking  the IsTitleBlockRevisionSchedule property, pretty handy hey. If it is, it’s passed over. If not, we get the schedules location using the Point property and storing it to schedLoc variable. Then we get the Id of the new sheet (nSheetId) and use these details to create a new schedule instance on the new sheet by using the Create function of the ScheduleSheetInstance class. The parameters of this function being the document (doc), new sheet Id (nSheetId), Id of the schedule (ele.ScheduleId) and the location of the new schedule instance (schedLoc). This new schedule is then appended to the output list (out).


After we’ve created and placed the new schedule instance, we can end the current Transaction and move onto the next sheet in the loop. If the toggle is set to False in the input the a string is output.


I have highlighted here an imported assembly that I don’t usually use however, I also used it in Module 8. Basically, we are imported the List class from the System.Collections.Generic namespace which will allows us to use this object later in the script.


Exactly as we did in part 6.1, we assign the inputs, check the toggle and create a for loop through the original sheets and new. A collector is also created to gather all the elements on the original sheet.


With all the sheet elements stored in the collector variable, we need to loop through them all to find the elements which match the Category.Name  of Text Notes or Lines. This is very similar as what was done in part 6.2 however, we are looking for detail items here and not schedules. If any of the elements match those specifications, they are added to the elesToCopy list that we have just created.


Here we need to use the List class that we imported earlier to create a list of Element Ids that inherits from the ICollection type object which the Revit API requires in some cases. So, by using List[ElementId] function and the list of element Ids we obtained earlier (elesToCopy) , we can cast our list to this new type.


Now that we have our list of elements that we want to copy (eleList), we can start a transaction to start copying them in Revit. We then need to check there are actually elements in the list by using Count to check the number of items. If the number of items does not equal to 0 then we can copy the elements. This is done using CopyElements function in the ElementTransformUtils class. The parameters for this function are the view that we are copying from (sheet), the ICollection of elements we want to copy (eleList), the new view to copy to (nSheet) and and transform or copy past options we want to use which in this case is None.


The result of the copied elements is now stored in the list ids. In order to get the actual elements for output, we need to run another quick little loop to get the actual copied elements, should we need to use them later. To do this, we create the for loop through the ids list and with each loop, use the GetElement method with the id as the parameter to get the Revit element and append it to the out list. 

If there were never any elements collected in eleList, the else statement now runs which appends a string to the output list (out) and the transaction ends.


Finally, if the input toggle variable happens to be False then the usual happens and a string is appended to the output list (out).

If all goes to plan then we should now have a script that duplicates sheets along with its views, schedules and detail items. I’ve found this little script to be quite useful when I’ve needed to quickly duplicate a sheet and its contents in the exact shape that it is currently in, so hopefully you find it helpful too. A little heads up though that each time you run this script, you will be copying sheets and views so please be mindful of that. If you want to rerun with just a minor tweak, I recommend cleaning up the drawings and sheets you would have created when running the script the first time. 

As always, if there is anything that didn’t quite make sense or you got stuck in following it along the way, please don’t hesitate to get in touch as I am always happy to help out. Also, if there’s anything I missed in terms of functionality, please let me know as I’m always looking to improve the scripts I work on. If you need to get in touch, you can contact me at or @LearnDynamo on Twitter. Cheers and please subscribe below for the latest on upcoming modules.




  1. Hung Pham
    November 1, 2016 @ 6:54 am

    Good jobs Jeremy. Thanks you so much.


    • Jeremy
      November 1, 2016 @ 7:39 am

      Thanks mate!


  2. Yien
    November 4, 2016 @ 8:41 pm

    works like a charm Jeremy!

    simple question : in Python, why don’t just import all the classes and modules and set it for good? im asking, Because, depending of the author and the nodes, sometimes there is import “Math”, UIapp, RevitAPI and some don’t.

    thank you for the explanation.



  3. alberto tono
    November 16, 2016 @ 8:49 pm

    Thanks, like always Jeremy!!! 😉


  4. Sebastian
    May 16, 2017 @ 7:51 pm

    Im getting this error:

    IronPythonEvaluator.EvaluateIronPythonScript operation failed.
    Traceback (most recent call last):
    File “”, line 20, in
    AttributeError: ‘BuiltInCategory’ object has no attribute ‘OfClass’

    Any help?

    : )


Leave a Reply

Your email address will not be published. Required fields are marked *