Shared Project Parameter GUID Reporter

I am still busy on the Revit API discussion forum, when not involved in other important seasonal preparations:

Pepparkakor

Now let's look at the overdue solution to determine the GUID of a shared project parameter:

Issue

Here is an rather overdue issue – from 2013 – that recently surfaced again.

A long discussion between CoderBoy, Scott Wilson and Ning Zhou on reporting on project parameter definitions – need GUIDs was resolved back then and never published by The Building Coder, in spite of best intentions of doing so.

It came up again in the recent discussion on creating a project parameter with "Values can vary by group instance" selected.

I now finally get around to publishing CoderBoy's solution.

In his own words:

In reference to your recent Revit API forum request, I worked the code into a functioning Revit command.

As such, I have attached a class written generally in the style of the Building Coder Samples which gathers all the data from the project parameters and puts them in a tab-delimited string format into the clipboard, which is suitable for pasting into a spreadsheet.

This code may be modified and/or freely distributed to the world.

When run on the out-of-the-box Revit sample project 'rme_basic_sample_project.rvt' the user sees this:

Project parameter GUID reporter

Pasting into a spreadsheet results in this:

Project parameter GUID report

Thank you very much, CoderBoy, and also Scott and Ning, of course!

Implementation

Here is the complete command implementation.

It provides an interesting and important read, with quite extensive documentation of some otherwise rather mystifying steps:

[Transaction( TransactionMode.Manual )]
class CmdProjectParameterGuids : IExternalCommand
{
  #region Data holding class
  /// <summary>
  /// This class contains information discovered 
  /// about a (shared or non-shared) project parameter 
  /// </summary>
  class ProjectParameterData
  {
    public Definition Definition = null;
    public ElementBinding Binding = null;
    public string Name = null;                // Needed because accsessing the Definition later may produce an error.
    public bool IsSharedStatusKnown = false// Will probably always be true when the data is gathered
    public bool IsShared = false;
    public string GUID = null;
  }
  #endregion // Data holding class
 
  #region Private helper methods
  /// <summary>
  /// Returns a list of the objects containing 
  /// references to the project parameter definitions
  /// </summary>
  /// <param name="doc">The project document being quereied</param>
  /// <returns></returns>
  static List<ProjectParameterData>
    GetProjectParameterData(
      Document doc )
  {
    // Following good SOA practices, first validate incoming parameters
 
    if( doc == null )
    {
      throw new ArgumentNullException( "doc" );
    }
 
    if( doc.IsFamilyDocument )
    {
      throw new Exception( "doc can not be a family document." );
    }
 
    List<ProjectParameterData> result
      = new List<ProjectParameterData>();
 
    BindingMap map = doc.ParameterBindings;
    DefinitionBindingMapIterator it
      = map.ForwardIterator();
    it.Reset();
    while( it.MoveNext() )
    {
      ProjectParameterData newProjectParameterData
        = new ProjectParameterData();
 
      newProjectParameterData.Definition = it.Key;
      newProjectParameterData.Name = it.Key.Name;
      newProjectParameterData.Binding = it.Current
        as ElementBinding;
 
      result.Add( newProjectParameterData );
    }
    return result;
  }
 
  /// <summary>
  /// This method takes a category and information 
  /// about a project parameter and adds a binding 
  /// to the category for the parameter.  It will 
  /// throw an exception if the parameter is already 
  /// bound to the desired category.  It returns
  /// whether or not the API reports that it 
  /// successfully bound the parameter to the 
  /// desired category.
  /// </summary>
  /// <param name="doc">The project document in which the project parameter has been defined</param>
  /// <param name="projectParameterData">Information about the project parameter</param>
  /// <param name="category">The additional category to which to bind the project parameter</param>
  /// <returns></returns>
  static bool AddProjectParameterBinding(
    Document doc,
    ProjectParameterData projectParameterData,
    Category category )
  {
    // Following good SOA practices, first validate incoming parameters
 
    if( doc == null )
    {
      throw new ArgumentNullException( "doc" );
    }
 
    if( doc.IsFamilyDocument )
    {
      throw new Exception(
        "doc can not be a family document." );
    }
 
    if( projectParameterData == null )
    {
      throw new ArgumentNullException(
        "projectParameterData" );
    }
 
    if( category == null )
    {
      throw new ArgumentNullException( "category" );
    }
 
    bool result = false;
 
    CategorySet cats = projectParameterData.Binding
      .Categories;
 
    if( cats.Contains( category ) )
    {
      // It's already bound to the desired category.  
      // Nothing to do.
      string errorMessage = string.Format(
        "The project parameter '{0}' is already bound to the '{1}' category.",
        projectParameterData.Definition.Name,
        category.Name );
 
      throw new Exception( errorMessage );
    }
 
    cats.Insert( category );
 
    // See if the parameter is an instance or type parameter.
 
    InstanceBinding instanceBinding
      = projectParameterData.Binding as InstanceBinding;
 
    if( instanceBinding != null )
    {
      // Is an Instance parameter
 
      InstanceBinding newInstanceBinding
        = doc.Application.Create
          .NewInstanceBinding( cats );
 
      if( doc.ParameterBindings.ReInsert(
        projectParameterData.Definition,
        newInstanceBinding ) )
      {
        result = true;
      }
    }
    else
    {
      // Is a type parameter
      TypeBinding typeBinding
        = doc.Application.Create
          .NewTypeBinding( cats );
 
      if( doc.ParameterBindings.ReInsert(
        projectParameterData.Definition, typeBinding ) )
      {
        result = true;
      }
    }
    return result;
  }
 
  /// <summary>
  /// This method populates the appropriate values 
  /// on a ProjectParameterData object with 
  /// information from the given Parameter object.
  /// </summary>
  /// <param name="parameter">The Parameter object with source information</param>
  /// <param name="projectParameterDataToFill">The ProjectParameterData object to fill</param>
  static void PopulateProjectParameterData(
    Parameter parameter,
    ProjectParameterData projectParameterDataToFill )
  {
    // Following good SOA practices, validate incoming parameters first.
 
    if( parameter == null )
    {
      throw new ArgumentNullException( "parameter" );
    }
 
    if( projectParameterDataToFill == null )
    {
      throw new ArgumentNullException(
        "projectParameterDataToFill" );
    }
 
    projectParameterDataToFill.IsSharedStatusKnown = true;
    projectParameterDataToFill.IsShared = parameter.IsShared;
    if( parameter.IsShared )
    {
      if( parameter.GUID != null )
      {
        projectParameterDataToFill.GUID = parameter.GUID.ToString();
      }
    }
  }  // end of PopulateProjectParameterData
  #endregion // Private helper methods
 
  public Result Execute(
    ExternalCommandData commandData,
    ref string message,
    ElementSet elements )
  {
    UIApplication uiapp = commandData.Application;
    UIDocument uidoc = uiapp.ActiveUIDocument;
    Document doc = uidoc.Document;
 
    if( doc.IsFamilyDocument )
    {
      message = "The document must be a project document.";
      return Result.Failed;
    }
 
    // Get the (singleton) element that is the 
    // ProjectInformation object.  It can only have 
    // instance parameters bound to it, and it is 
    // always guaranteed to exist.
 
    Element projectInfoElement
      = new FilteredElementCollector( doc )
        .OfCategory( BuiltInCategory.OST_ProjectInformation )
        .FirstElement();
 
    // Get the first wall type element.  It can only 
    // have type parameters bound to it, and there is 
    // always guaranteed to be at least one of these.
 
    Element firstWallTypeElement
      = new FilteredElementCollector( doc )
        .OfCategory( BuiltInCategory.OST_Walls )
        .WhereElementIsElementType()
        .FirstElement();
 
    CategorySet categories = null;
    Parameter foundParameter = null;
 
    // Get the list of information about all project 
    // parameters, calling our helper method, below.  
 
    List<ProjectParameterData> projectParametersData
      = GetProjectParameterData( doc );
 
    // In order to be able to query whether or not a 
    // project parameter is shared or not, and if it 
    // is shared then what it's GUID is, we must ensure 
    // it exists in the Parameters collection of an 
    // element.
    // This is because we cannot query this information 
    // directly from the project parameter bindings 
    // object.
    // So each project parameter will attempt to be 
    // temporarily bound to a known object so a 
    // Parameter object created from it will exist 
    // and can be queried for this additional 
    // information.
 
    foreach( ProjectParameterData projectParameterData
      in projectParametersData )
    {
      if( projectParameterData.Definition != null )
      {
        categories = projectParameterData.Binding.Categories;
        if( !categories.Contains( projectInfoElement.Category ) )
        {
          // This project parameter is not already 
          // bound to the ProjectInformation category,
          // so we must temporarily bind it so we can 
          // query that object for it.
 
          using( Transaction tempTransaction
            = new Transaction( doc ) )
          {
            tempTransaction.Start( "Temporary" );
 
            // Try to bind the project parameter do 
            // the project information category, 
            // calling our helper method, below.
 
            if( AddProjectParameterBinding(
              doc, projectParameterData,
              projectInfoElement.Category ) )
            {
              // successfully bound
              foundParameter
                = projectInfoElement.get_Parameter(
                  projectParameterData.Definition );
 
              if( foundParameter == null )
              {
                // Must be a shared type parameter, 
                // which the API reports that it binds
                // to the project information category 
                // via the API, but doesn't ACTUALLY 
                // bind to the project information 
                // category.  (Sheesh!)
 
                // So we must use a different, type 
                // based object known to exist, and 
                // try again.
 
                if( !categories.Contains(
                  firstWallTypeElement.Category ) )
                {
                  // Add it to walls category as we 
                  // did with project info for the 
                  // others, calling our helper 
                  // method, below.
 
                  if( AddProjectParameterBinding(
                    doc, projectParameterData,
                    firstWallTypeElement.Category ) )
                  {
                    // Successfully bound
                    foundParameter
                      = firstWallTypeElement.get_Parameter(
                        projectParameterData.Definition );
                  }
                }
                else
                {
                  // The project parameter was already 
                  // bound to the Walls category.
                  foundParameter
                    = firstWallTypeElement.get_Parameter(
                      projectParameterData.Definition );
                }
 
                if( foundParameter != null )
                {
                  PopulateProjectParameterData(
                    foundParameter,
                    projectParameterData );
                }
                else
                {
                  // Wouldn't bind to the walls 
                  // category or wasn't found when 
                  // already bound.
                  // This should probably never happen?
 
                  projectParameterData.IsSharedStatusKnown
                    = false// Throw exception?
                }
              }
              else
              {
                // Found the correct parameter 
                // instance on the Project 
                // Information object, so use it.
 
                PopulateProjectParameterData(
                  foundParameter,
                  projectParameterData );
              }
            }
            else
            {
              // The API reports it couldn't bind 
              // the parameter to the ProjectInformation 
              // category.
              // This only happens with non-shared 
              // Project parameters, which have no 
              // GUID anyway.
 
              projectParameterData.IsShared = false;
              projectParameterData.IsSharedStatusKnown = true;
            }
            tempTransaction.RollBack();
          }
        }
        else
        {
          // The project parameter was already bound 
          // to the Project Information category.
 
          foundParameter
            = projectInfoElement.get_Parameter(
              projectParameterData.Definition );
 
          if( foundParameter != null )
          {
            PopulateProjectParameterData(
              foundParameter, projectParameterData );
          }
          else
          {
            // This will probably never happen.
 
            projectParameterData.IsSharedStatusKnown
              = false// Throw exception?
          }
        }
 
      }  // Whether or not the Definition object could be found
 
    }  // For each original project parameter definition
 
    StringBuilder sb = new StringBuilder();
 
    // Build column headers
 
    sb.AppendLine( "PARAMETER NAME\tIS SHARED?\tGUID" );
 
    // Add each row.
 
    foreach( ProjectParameterData projectParameterData
      in projectParametersData )
    {
      sb.Append( projectParameterData.Name );
      sb.Append( "\t" );
 
      if( projectParameterData.IsSharedStatusKnown )
      {
        sb.Append( projectParameterData.IsShared.ToString() );
      }
      else
      {
        sb.Append( "<Unknown>" );
      }
 
      if( projectParameterData.IsSharedStatusKnown &&
          projectParameterData.IsShared )
      {
        sb.Append( "\t" );
        sb.Append( projectParameterData.GUID );
      }
      sb.AppendLine();
    }
 
    System.Windows.Clipboard.Clear();
    System.Windows.Clipboard.SetText( sb.ToString() );
 
    TaskDialog resultsDialog = new TaskDialog(
      "Results are in the Clipboard" );
 
    resultsDialog.MainInstruction
      = "Results are in the Clipboard";
 
    resultsDialog.MainContent
      = "Paste the clipboard into a spreadsheet "
        + "program to see the results.";
 
    resultsDialog.Show();
 
    return Result.Succeeded;
  }
}

Download

I integrated CoderBoy's code into The Building Coder samples release 2016.0.125.2, in the module CmdProjectParameterGuids.

Discussion

Here is a summary of the discussion leading up to this.

It provides some interesting insight into all the work, research and collaboration required for this implementation.

Question: We have a need to report the project parameters that are defined in a project, as part of a Quality Assurance tool on which we are working.

While Revit apparently won't let you create two (non-shared) project parameters with the same, case-sensitive name, for some reason it WILL let you add two Shared Parameters with identical, case-sensitive names provided they have each have different GUIDs.

Each of those parameters may be of the same or different types, in the same or different groups, and associated with the same or different categories. So yes, you could have two parameters with identical names appear in the same group of the properties pane for an element, right next to each other.

So for us to accurately report the project parameters that are defined, we have to differentiate between the two definitions. However, those definitions may be identical with the only difference (we can tell) being the shared parameter GUIDs.

But there doesn't seem to be a way to ask for the GUID of a project parameter with the API, not even when we try to resort to using Reflection. Every time we try casting the DefinitionBiningMapIterator.Key to ExternalDefinition we get null, even for parameters we know by testing are shared. It always casts to InternalDefinition, but never to ExternalDefinition. So not only can we not seem to get the GUID, there doesn't seem to be a way to even tell if it's a shared parameter or not in the first place.

What are we missing? Is there some other API methods we simply haven't found yet to get this information? How can we accurately report how the project parameters were defined by the user in the GUI?

Thanks very much for any assistance you can provide.

Answer: Parameter.GUID is the property you seek. Armed with the correct GUID from the shared parameters text file you can verify that the correct parameter is found. Be sure to test the Parameter.IsShared property before accessing the GUID property as non-shared parameters will throw an exception.

That works for shared parameters, if you are looking for a non-shared project parameter then I guess you could filter out all shared ones of the same name, with the remaining non-shared one being your target.

Response: Yes, that is how we do it for regular Element parameters.

But have you actually tried this on project parameters? To the best of my knowledge project parameters do not inherit from the Parmeter class. I believe they are a very different beast, I think in part because they can be associated with one or more categories, which regular Parameter objects don't do.

For brief example, it is my understanding that accessing project parameters starts with the document ParameterBindings property, leading to Definition instances.

None of the properties on the Definition object are of type Parameter, nor can they seem to cast to a Parameter object, unless I'm missing something.

Further, there is absolutely no guarantee that the current shared parameters file (if any) was used to provide the shared parameters to the project parameters. Imagine, for example, you are an outside party who got the project file from the architect and you don't have their shared parameters file, or the correct one or more of the many shared parameters files they may be using. WORSE, what if a different shared parameters file is currently being used and it has a shared parameter in it with the same name and other properties but a different GUID? We'd wind up reporting the wrong shared parameter definition being used in the project, which violates the requirements.

Unless you can guarantee an extremely well controlled environment (we can't), you should never assume the definitions in a shared parameters file were the ones actually used in a project.

Also, a shared parameters file can't have two parameters with the exact same case-sensitive name in it, whereas the project parameters list CAN have two or more parameters with the exact same case-sensitive name in it, provided they were shared parameters and had different GUIDs. We've actually seen this in one of our customers projects, which is where the need to accurately report this information came from. So we really have a need to get the actual GUID being used inside the Revit project for the project parameter definition so we can properly report what is going on, and where things came from.

While I greatly appreciate the effort (thanks!), I can't find a way to reliably apply anything you've suggested.

If you can provide a snippet of code that works reliably on project parameters, I would be hugely grateful.

Thanks again.

Answer: Ok, I've been meaning to do something like this for a while now so I just knocked up a quick demo of how to get a complete list of both project and shared parameters used within a document. It's a little crude but it'll do the job.

It basically analyses the parameters of every element within a document and generates a list of unique parameters found and categorises them into shared or project parameters. It also appends the GUID as a hex string to the end of all shared parameter names. If you run it on a document that has multiple parameters with the same name (Both shared and project) you will see that it identifies each individually.

... sample code...

As you can see, there is no need to be messing with binding maps as all the data you need is found within the parameters attached to the elements. There isn't any way to get the GUID for the project parameters, but as you have already noted, each project parameter must have a unique (case-sensitive) name so it shouldn't matter.

Response: Again, I hugely appreciate the effort, but there are some significant issues:

  1. [Environment clarification] > "...each project parameter must have a unique (case-sensitive) name so it shouldn't matter."

That statement is not true, and not what I tried (perhaps unsuccessfully?) to note. More than 1 project parameter can have the exact same case-sensitive name, provided they are shared and have different GUIDs. (If they're not shared, then yes the names must be unique – go figure!!!)

So, for example, I could have a project parameter that is a shared parameter called "Voltage" with the GUID 8AB7097C-015A-4FEB-99A9-585882951695, and I could add another project parameter that is a shared parameter (from a different shared parameters file) called "Voltage" with the different GUID 3BDF50E5-AC43-43CA-897E-46118EDBD066 and both would show up on the list of project parameters as "Voltage"

Worse, if they're in the same category and in the same group, in the Properties panel for an element of that category, the Revit user would actually see this:

  Voltage
  Voltage

If they're not in the same group, one could (for example) be at the top of the list and one could be at the bottom of the list, and the Revit user would probably just fill in whichever one they happened to see first. YES, we have at least 1 (very big) customer with this messed-up situation in their projects.

(And which one do you pick if you're trying to build a schedule???)

If those hadn't been shared parameters, and were just regular, non-shared project parameters, then Revit would not allow me to create a second project parameter called "Voltage" (again, go figure!). But if they're shared, I can have 2 of them. Or any number of them, if they all have different GUIDs.

However, what I also noted was that in a single shared parameters file each parameter definition must have a unique (case-sensitive) name. So it's easy to see how that could get confusing. (This whole Revit "feature" is messed up and confusing)

  1. Perhaps the biggest issue with this example code is that the code for this approach presumes (and requires) that family instances have been created for at least 1 category attached to every project parameter, and by extension that families actually already exist in the project for at least 1 category attached to every project parameter. That simply may not be the case. Instance project parameters can be defined and associated to categories for which no instances have yet to be created. There may not even be any families yet loaded into the project of those categories as well. A use case for this tool may be, for example, to QA check a project file that is used as a template, or one that is early in the project lifecycle.

  2. The code in the example appears to assume that if it's a shared parameter, it's not a project parameter. It further appears to assume that if it's not a shared parameter, it must be a project parameter. Neither of these assumptions is likely to ever be correct, or would only be correct under extremely tightly controlled circumstances, which unfortunately we don't have. Our code needs to work generically, on any project.

  3. And not at all insignificantly, wouldn't iterating over every parameter on every element in a project be OMG unbelievably miserably sloooow, especially for large projects?

The idea is very creative to be sure, and again the effort is very much appreciated, but unfortunately it doesn't seem to be robust enough to handle any project accurately, which is what we need.

Surely there's a better, more reliable/accurate way to get project parameter definition information, including whether or not a project parameter definition is shared and what its GUID is...?

Again, many thanks for your efforts!

Answer: Did you run the code?

You seem confused regarding the differences between project parameters and a shared parameters. Maybe we are working from different terminolgy. "Project Parameters" are by definition not shared if you take Revit's 'Parameter Properties' dialog as a reference. If you are simply refering to paramaters that are present within a project document then I can see where the confusion might be, but why not just call them Parameters?

In the context of the definition above, my statement regarding project parameters (Non-Shared) requiring unique names is accurate. It is the shared parameters that can have common naming, but it is easy to differentiate them using thier GUID. At present the only way I know to get the GUID of a shared parameter without access to the correct external parameters file is to find an element that uses the parameter. This is what my example code demonstrates.

You say that you appreciate the help, but you then proceed to complain about my contribution in point form? Good luck solving your problem, I'll leave you to it then.

Response: I really do very much appreciate your time and efforts, but I agree that we probably have different understandings of the situation.

"Project Parameters" are by definition not shared if you take Revit's 'Parameter Properties' dialog as a reference.

Here is an image of the Revit Project Parameters 'Parameter Properties' dialog that highlights what I mean by having a project parameter that comes from a shared parameters file. The "Shared parameter" radio button area is circled:

Parameter Properties

Maybe this will help clear up the confusion?

Your code does correctly demonstrate getting the GUID for a shared parameter from the project when it is in the Parameters collection of an instance element. I did know how to get that information.

But as my earlier message with sample data mentions, I need to be able to get that information for shared instance project parameter definitions from the project when there are no instances existing in the project, or possibly even any families loaded into the project for any categories associated with the project parameter. (That would prevent finding Type project parameter GUIDs, or the ability to create throwaway instances that can be created, queried and discarded...in other words, the data needed to "cheat" may not exist in the project to begin with).

That is the problem I need to solve. I hope between the sample data and the attached images that what I need to do is easier to understand.

Answer:: I just had the thought that maybe you could temporarily bind a parameter definition to the "Project Information" category which would then make it accessible from the Document.ProjectInformation element. You can then analyse it using the methods discussed earlier to determine whether it is shared or not and get the GUID if available. I haven't tried this yet so I'm not sure if it would work, worth a try I guess.

Well it turns out that temporarily binding a shared parameter definition to BuiltInCategory.OST_ProjectInformation and then reading the ProjectInformation element's parameters to get at the GUID works just fine. I was unable to bind any non-shared parameters using the API which is as I was expecting due to the limited functionality available for dealing with non-shared parameters.

I'm not 100% certain, but I think it is fair to say that if a parameter definition rejects binding to a category, then it must be a non-shared project parameter, while any that succeed are shared parameters (which can be confirmed by checking for IsShared just to be sure).

Assuming the above rule holds true, all parameter definitions found within Document.ParameterBindings can then be categorised as shared or non-shared. In the event that several parameters are found with the same name (case-sensitive), any that are shared can be uiquely identified using the GUID and there will be at most 1 non-shared parameter which can be uniquely identified within the group based on its non-shared status.

That should get you motoring along.

By the way I'd like to thank Jeremy for his blog post and code for adding a category to a shared parameter binding, which I used to cobble together a quick test rig for the binding.

Response: While it's unfortunate that we have to play games like this, the approach sounds utterly brilliant. I had forgotten that the ProjectInformation object is effectively like a singleton – always exactly 1 element of it exists.

It's the perfect place to try to do something like this.

I'll work on integrating this into my code today, but it sounds extremely promising. I'll let you know how it goes.

Thank you so very much!

Well, it looks like this approach should work, but only for some conditions:

  1. I think it will only work on instance parameters. This is based on the the fact that Project Information is not a category choice in the Project Parameters dialog for type parameters. Presuming that carries through in the API, now I don't know what to do about type shared parameters.

  2. If two or more instance parameters with identical names are already associated with the Project Information category before we start, there won't be a way to tell the existing parameters in the Project Information apart if they have the same parameter type, parameter group, etc (which they very well may). This might be resolvable by trying to temporarily remove all but one of the duplicate project parameters from the Project Information category, and querying the only one left. I'm not sure how to do that yet because I haven't played around with dissociating a category from a project parameter.

So it seems by attempting this approach (using the Project Information "singleton" to store Parameter instances) that accurately reporting the GUIDs for instance parameters may be possible, but it will be a LOT of work checking for and handling duplicates already assigned to that category to ensure accuracy. At this time I still don't know what to do about getting the GUIDS for type shared project parameters.

Answer:: I was just looking into the Type Parameter issue and stumbled upon some useful but also very dangerous API behavior.

The good news is that the API will happily let you rebind a shared 'type' parameter as an instance parameter using BindingMap.ReInsert. This is great as it will then show up on the ProjectInformation element and reveal its GUID.

The problem is that any type information already stored within the project is now deleted and lost forever due to the change from instance to type binding...

This is easily cured by doing a rollback on the transaction, but be careful using this technique as it could ruin the project file if the transaction is accidentally comitted by some other part of your code. I'd suggest placing the binding and GUID investigation code within its own method encapsulated by a sub transaction and ensure that it is rolled back properly before returning. This is probably obvious advice, but better safe than sorry.

Response: Well, after wasting about 2 days trying to hack around these deficiencies in the Revit API for a solution to this problem, we have decided to cut our losses and abandon it. If we see duplicate project parameter names we will simply report they exist, and what they are, and walk away.

The problems really come in when you try to write code that will work under all circumstances for any project. For just one example, (instance) project parameters (shared or not) with duplicate names may already be bound to the Project Information category before you even start. So an approach we tried is in a temporary transaction to be rolled back, to remove all bindings for the ProjectInformation category for all project parameters, and then when it's time to try to query a Parameter guid from the ProjectInformation object, rebind the project parameters one at a time to the ProjectInformation category so there will never be more than 1 with that exact name in the ProjectInformation object's parameters collection. This would guarantee the right Parameter object for each project parameter being tested.

But unbinding them with the API seems to only actually work for shared project parameters. Non-shared ones simply refuse to unbind. When you remove the ProjectInformation category from what turns out to be a non-shared project parameter and try to unbind it, it refuses to unbind but the categories collection for that parameter still shows the ProjectInformation category as NOT being in the list, even though it really is bound to the ProjectInformation object if you look at it in the GUI after committing the test transaction. UGGGH!!!!

Plus, when trying to do some of these things the DefinitionBindingMapIterator.MoveNext() method throws exceptions (exactly when is a blur right now). So apparently there are some things you can do to a project parameter during the iteration, and some things you can't.

We've already spent far more time fighting the Revit API than management feels it is worth for whatever benefit we might gain by trying to report the GUIDs for shared project parameters.

Scott: thanks again for the great ideas. Valiant effort. It may be possible to reliably report the GUIDs for all shared project parameters for any project under any configuration with the Revit 2014 API, but I'll only believe it when I see it.

I hate to leave a problem like this unsolved, but we also have to do what we're told.

Answer: It's unfortunate that you have to quit when you are so close. Oh well, it's been a fun little project for me, I've been wanting to learn more about parameters from the API side of things for a while now and this topic gave me the incentive to dive in.

I have some ideas on how to solve the duplicate names already bound problem also, but I'll keep them to myself for now until I investigate them fully.

Answer 2: BindingMap.ReInsert works well, for while it.MoveNext throws an exception, I used the following workaround:

public struct MyData
{
   public Definition def;
   public ElementBinding binding;
}

while (it.MoveNext())
{
   MyData myData;
   myData.def = it.Key;
   myData.binding = it.Current as ElementBinding;
   myDatas.Add(myData);
}

foreach (MyData md in myDatas)
{
   // do normal processing
}

The final result looks like this:

Shared project parameter GUID

Response: I had thought of an approach like that for solving the iteration exception problem, but was having other issues getting Revit to behave correctly. Would it be possible for you to provide all the code you used in your test (a zip file would be fine)?

If it looks close to what we need it to do (including working under all conditions) we may be able to convince management to give us another couple of hours to try to perfect this for our situation. Your code may have also resolved other issues we have run into, providing we can get things to rollback cleanly as well.

Regardless, it could be a very good learning exercise to see how your code works.

At the very least, thank you very much for your idea!

Answer: No problem, let me know if it works or there's any issue or better way

Thanks you guys, especially Scott, for the discussion of this interesting topic!

Answer 2: I think the reason that DefinitionBindingMapIterator MoveNext was throwing exceptions is that you were messing with binding maps while still looping through them. It's similar to attempting to add/remove a list item while within a foreach loop operatiing on the same list.

I suspect that rolling back an enclosed transaction doesn't work either as the BindingMap is probably re-built in memory by the API at each change regardless of roll-backs, invalidating the iterator's pointers.

NingZhou's code solves the issue nicely by caching the definitions before looping through them. Rolling back at each loop is also a good safety measure to ensure safety of existing data. Getting the parameter from the ProjectInformation element by Definition instead of name string also solves your duplicate naming problem.

I think that about covers it, nice work.

Hmm, just had a thought to add GUID and IsShared as extensions to the Definiton class.

Response: Wow! Thank you very much for providing this code.

I haven't had a chance to look at this today, but am hoping to look at it again in the next couple of days.

It's greatly appreciated!

This code does appear to work IF none of the project parameters are already bound to the ProjectInformation category before it starts to run.

What we eventually need to do is report which categories are associated with each project parameter (name & GUID if shared) or (name if not shared, in which case there can be only 1 with that name that is not shared).

So just reporting name-GUID combinations isn't enough, we also need to report which categories go with each exact parameter definition. So it needs to be precise.

The attached project file presents this situation. Each project parameter (shared or not) is bound to both the Project Information category and the Rooms category. Again, I'm having issues getting things unbound cleanly.

However, I have another thought. What if I pre-save the definitions (ala the MyData approach), then using temporary, rolled back transactions, iterate over that list for each item, deleting all but the one project parameter of interest, bind it to the ProjectInformation category (if it isn't already) and then query the ProjectInformation object for the parameter by name?

So I may try that next. Not sure what will happen to the objects in my list if I delete their parent definitions, though. I won't be needing them during the deleted time, so maybe that will work.

(Yes, there are built-in Project Information parameters like "Organization Name" whereby if I added my own Project Parameter with that name, it could be a duplicate in the Project Information object as well, not sure if I can filter those out by BuiltInParameter or not...I'll try to cross that bridge if I can get to it)

Solution

The code discussed thus far seems to work correctly for all situations except when there is a shared project parameter that is a type parameter.

The API reports that a shared project type parameter binds successfully to the ProjectInformation category, but the associated Parameter does not actually exist in the Project Information object's Parameters collection. This makes some sense, because you can't bind any project parameters (shared or not) that are type parameters to the Project Information category in the Revit GUI. (So in this case the API wrongly reports the binding is successful)

The approach outlined thus far uses a known "singleton" (the ProjectInformation element) which works for instance parameters. So I extended that approach (when needed) to use a known "singleton" (the first Wall Type element, as there must always be at least one) which works for type parameters.

Attached is a text file GetProjectParameterGUIDs.txt with C# snippets that reasonably nicely packages all of the code needed to accurately collect information about project parameters, whether or not they are shared, and if shared what their GUIDs are.

Again, many many thanks to Scott Wilson and Ning Zhou for their ideas and code snippets to help solve this problem!