Adding a category to a project parameter

Adding a category to a project parameter

r.terry
Explorer Explorer
2,788 Views
9 Replies
Message 1 of 10

Adding a category to a project parameter

r.terry
Explorer
Explorer

When I want to add a category to a project parameter that already exists in a Revit file, simply inserting the category into the parameter that exists in the "Document.ParameterBindings" property does not actually add that category, and the box for that category in the project parameter definitions is not checked. It seems to me that something like the code below should be sufficient for adding the category to the parameter:

 

ElementBinding b = (ElementBinding)elem.Document.ParameterBindings.get_Item(definition);

b.Categories.Insert(elem.Category);

 

This is something that is easily available in the UI, but does not seem to function the same through the API.

 

So, I use the "ReInsert" function of the "BindingMap" class to get the parameter to update. What I am finding is that when I perform this action on multiple different occasions, applying new categories to the same parameter(s), some of the elements that already had a value for the parameter(s) will lose the value(s). It is an inconsistent behavior, but performing the function multiple times seems to always end up in a loss of data on at least some of the elements. This prevents functionality of some of our custom workflows, so I am going to have to write a custom reinsert function that will save and rewrite all values of the parameter if changed.

 

What I am wondering is if there is a better way to add a category to a project parameter that I am overlooking, or if the API is not working the way it was intended to (or should) work.

2,789 Views
9 Replies
Replies (9)
Message 2 of 10

Anonymous
Not applicable

@r.terry 
While i cannot speak for whether there is a bug or not, i can tell you a way to compensate for the data loss.

 

These are parameters, right? You are just losing values but the definitions are in place?

 

If these values can be calculated or deduced from available properties or anything you have access to, then implementing an IUpdater would be the most straightforward way to get those values back in place. Then you do not have to worry about saving them off, they will just update again after the definition changes.

 

If not, and they really need to be saved off, there are a couple ways to go about it but if you get stuck i could make a suggestion there.

 

Not an answer, just offering a thought.

 

Thanks,

 

Matt

0 Likes
Message 3 of 10

prasadNELXB
Explorer
Explorer

 

@r.terry  I am experiencing the exact same problem i.e loosing data on some of the elements after "ReInsert"

 

Just wondering did you found out any thing more of this? or any work around? 

0 Likes
Message 4 of 10

BobbyC.Jones
Advocate
Advocate

I ended up caching and reapplying the values after calls to ReInsert().

 

if (!document.ParameterBindings.Insert(parameterDefinition, binding))
{
    var parameterCache = new StringParameterValueCache(document, parameterDefinition, categories);
                
    parameterCache.CacheValues();

    document.ParameterBindings.ReInsert(parameterDefinition, binding);

    parameterCache.ReInsertValues();
}

 

I wanted to go back and make this a generic class template, but never had the need.  Perhaps you can use this as a starting point.

public class StringParameterValueCache
{
    private readonly Document _document;
    private readonly Definition _parameterDefinition;
    private readonly CategorySet _categories;
    private Dictionary<ElementId, string> _cachedValues;

    public StringParameterValueCache(Document document, Definition parameterDefinition, CategorySet categories)
    {
        _document = document;
        _parameterDefinition = parameterDefinition;
        _categories = categories;
    }

    public void CacheValues()
    {
        //Potential point of future update
        //parameters can hold: ElementId, int, double, string
        //this will need to be updated to support the types other than string
        if (_parameterDefinition.ParameterType != ParameterType.Text)
        {
            _cachedValues = new Dictionary<ElementId, string>();

            return;
        }

        var filter = GetElementsContainingParameterFilter();

        var values = _document.Collector()
            .WherePasses(filter)
            //this value getter must be updated to support types other than string
            .Select(e => new { Id = e.Id, Value = e.GetParameterValueAsString(_parameterDefinition.Name) })
            .Where(e => !string.IsNullOrWhiteSpace(e.Value))
            .ToDictionary(e => e.Id, e => e.Value);

        _cachedValues = values;
    }

    public void ReInsertValues()
    {
        foreach (var cachedValue in _cachedValues)
        {
            var element = _document.GetElement(cachedValue.Key);

            element.SetParameter(_parameterDefinition.Name, cachedValue.Value);
        }

        _cachedValues.Clear();
    }

    private ElementFilter GetElementsContainingParameterFilter()
    {
        //Category filter is a quick filter
        var categoryFilterRule = new FilterCategoryRule(_categories.Cast<Category>().Select(c => c.Id).ToList());
        var categoryFilter = new ElementParameterFilter(categoryFilterRule);

        //Parameter filter is a slow filter
        var parameterAppliesRule = new SharedParameterApplicableRule(_parameterDefinition.Name);
        var parameterAppliesFilter = new ElementParameterFilter(parameterAppliesRule);

        var combinedFilter = new LogicalAndFilter(categoryFilter, parameterAppliesFilter);
        return combinedFilter;
    }
}

 

--
Bobby C. Jones
Message 5 of 10

havard.leding
Contributor
Contributor

This seems to still be an issue in Revit 2022.

Has anyone tried 23/34 versions ?

I have a crazy amount of elements that would need a rewrite.

 

@Anonymous 

I upgraded your StringParameterValueCache class.
I figure since each "Parameter" knows which element it belongs to you only need to cache "Parameter" for rewrite.
Maybe it will run faster. 
But still in my case this could potentialy be the entire Revit model.

 

 

 public class StringParameterValueCache
    {
        private readonly Document _document;
        private readonly Definition _parameterDefinition;
        private readonly CategorySet _categories;
        private readonly Guid _sharedParameterGuid;
        private Dictionary<Parameter, string> _cachedValues;

        public StringParameterValueCache(Document document, Guid sharedParameterGuid, Definition parameterDefinition, CategorySet categories)
        {
            _document = document;
            _parameterDefinition = parameterDefinition;
            _categories = categories;
            _sharedParameterGuid= sharedParameterGuid;
        }

        public void CacheValues()
        {
            //Potential point of future update
            //parameters can hold: ElementId, int, double, string
            //this will need to be updated to support the types other than string
            if (_parameterDefinition.GetDataType() != SpecTypeId.String.Text)
            {
                _cachedValues = new Dictionary<Parameter, string>();

                return;
            }
            
            var filter = GetElementsContainingParameterFilter();

            var values = new FilteredElementCollector(_document)
                .WherePasses(filter)
                //this value getter must be updated to support types other than string
                .Select(t => t.get_Parameter(_sharedParameterGuid))
                .Where(t => t.HasValue)
                .ToDictionary(e => e, e => e.AsString());

            _cachedValues = values;
        }

        public void ReInsertValues()
        {
            foreach (var cachedValue in _cachedValues)
            {
                cachedValue.Key.Set(cachedValue.Value);
            }

            _cachedValues.Clear();
        }

        private ElementFilter GetElementsContainingParameterFilter()
        {
            //Category filter is a quick filter
            var categoryFilterRule = new FilterCategoryRule(_categories.Cast<Category>().Select(c => c.Id).ToList());
            var categoryFilter = new ElementParameterFilter(categoryFilterRule);

            //Parameter filter is a slow filter
            var parameterAppliesRule = new SharedParameterApplicableRule(_parameterDefinition.Name);
            var parameterAppliesFilter = new ElementParameterFilter(parameterAppliesRule);

            var combinedFilter = new LogicalAndFilter(categoryFilter, parameterAppliesFilter);
            return combinedFilter;
        }
    }

 

 

Message 6 of 10

havard.leding
Contributor
Contributor

This is how its gonna be 👍

(Caching / resetting values in 500mb+ Revit files could take a long time)


Warning.png

Message 7 of 10

gdavis479JP
Contributor
Contributor

I like the looks of this, seems closest to working for what I am trying to do so far on the forums. However, I get an error on .Collector() HERE:

  var values = _document.Collector()
                .WherePasses(filter)
                //this value getter must be updated to support types other than string
                .Select(e => new { Id = e.Id, Value = e.GetParameterValueAsString(_parameterDefinition.Name) })
                .Where(e => !string.IsNullOrWhiteSpace(e.Value))
                .ToDictionary(e => e.Id, e => e.Value);

 

and .SetParameter HERE:

element.SetParameter(_parameterDefinition.Name, cachedValue.Value);

 

Do you have a definition for those you could share, or can you sight the reference / using statement needed?

 

Thanks,

Greg

Message 8 of 10

BobbyC.Jones
Advocate
Advocate

Hello Greg,

Document.Collector() is an extension method that returns a FilteredElementCollector.  It's just a helper to reduce code clutter.

 

using Autodesk.Revit.DB;

namespace Extensions.Collectors
{
    public static class DocumentExtensions
    {
        public static FilteredElementCollector Collector(this Document document)
        {
            return new FilteredElementCollector(document);
        }
    }
}

 

Here's a youtube video that explains it in great detail
Filtered Element Collector QuickTip - YouTube

 

--
Bobby C. Jones
0 Likes
Message 9 of 10

BobbyC.Jones
Advocate
Advocate

And Element.SetParameter() is another extension helper method.

 

 

using System;
using Autodesk.Revit.DB;

namespace Extensions.Elements
{
    public static class ElementExtensions
    {
        public static bool SetParameter<T>(this Element element, string name, T value)
        {
            var parameter = element.LookupParameter(name);
            if (parameter == null) return false;

            switch (value)
            {
                case string s:
                    return parameter.Set(s);
                case int _:
                    return parameter.Set(Convert.ToInt32(value));
                case double _:
                    return parameter.Set(Convert.ToDouble(value));
                case ElementId id:
                    return parameter.Set(id);
                default:
                    return false;
            }
        }

    }
}

 

 

Note that this method utilizes the LookUpParameter() method.  If you're concerned about multiple parameters with the same name you'll need to adjust accordingly.

--
Bobby C. Jones
0 Likes
Message 10 of 10

carolina.machadoCQZBW
Explorer
Explorer

Hello! I have registered an idea to solve this issue.

The link is: https://forums.autodesk.com/t5/revit-ideas/change-categories-of-existing-project-parameters-via-api/...

 

 

0 Likes