diff --git a/CHANGELOG.md b/CHANGELOG.md index 589fad8..702c2a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,56 @@ - Fixed `GetRibbonPanels(Tab.AddIns)` and `CreatePanel` throwing `KeyNotFoundException` after `RemovePanel` removed the last panel of the "Add-Ins" or "Analyze" tab. +## Element lookup + +Element lookups follow the convention of LINQ `First` and `FirstOrDefault`. +`ToElement` and `AsElement` return the element or throw, and `ToElementOrDefault` and `AsElementOrDefault` return the element or `null`: + +| Member | No element | Element of another type | +|-----------------------------------------------|-----------------------------|-------------------------| +| `ToElement`, `ToElement` | `InvalidOperationException` | `InvalidCastException` | +| `AsElement`, `AsElement` | `InvalidOperationException` | `InvalidCastException` | +| `ToElementOrDefault`, `ToElementOrDefault` | `null` | `null` | +| `AsElementOrDefault`, `AsElementOrDefault` | `null` | `null` | + +Resolving an id that may reference another element used to drop out of the extensions: + +```c# +var view = document.GetElement(elementId) as View; +``` + +Now it is one call: + +```c# +var view = elementId.ToElementOrDefault(document); +if (view is null) +{ + return; +} +``` + +- `elementId.ToElementOrDefault(Document)` +- `elementId.ToElementOrDefault(Document)` +- `parameter.AsElementOrDefault()` +- `parameter.AsElementOrDefault()` + +## Breaking changes + +**`ToElement` and `AsElement` throw for a missing element.** +`ToElement`, `ToElement`, `AsElement`, and `AsElement` returned `null` when the id or the parameter referenced no element. +They throw `InvalidOperationException` for it, and their return types are not nullable. +Code that checks the result for `null` switches to the `OrDefault` counterpart: + +```c# +// Before +var level = parameter.AsElement(); +if (level is null) return; + +// After +var level = parameter.AsElementOrDefault(); +if (level is null) return; +``` + # 2027.0.5 - Fixed `CreatePanel` adding a duplicate panel on every call for a built-in tab addressed by its title when the title differs from the tab ID. diff --git a/README.md b/README.md index 15ca7dc..2e68d0b 100644 --- a/README.md +++ b/README.md @@ -112,13 +112,20 @@ Package included by default in [Revit Templates](https://github.com/Nice3point/R ## Element -**ToElement** retrieves the element associated with the specified ElementId. +**ToElement** retrieves the element associated with the specified ElementId, and throws when the ElementId references no element or an element of another type. ```csharp Element element = wallId.ToElement(document); Wall wall = wallId.ToElement(document); ``` +**ToElementOrDefault** retrieves the element, or `null` when the ElementId references no element or an element of another type. + +```csharp +Element? element = elementId.ToElementOrDefault(document); +View? view = elementId.ToElementOrDefault(document); +``` + **ToElements** retrieves a collection of elements associated with the specified ElementIds. ```csharp @@ -731,13 +738,20 @@ bool value = element.FindParameter("IsClosed").AsBool(); Color value = element.FindParameter("Door color").AsColor(); ``` -**AsElement** provides access to the Element within the parameter. +**AsElement** provides access to the Element within the parameter, and throws when the parameter references no element or an element of another type. ```csharp Element value = element.FindParameter("Door material").AsElement(); Material value = element.FindParameter("Door material").AsElement(); ``` +**AsElementOrDefault** provides access to the Element within the parameter, or `null` when the parameter references no element or an element of another type. + +```csharp +Element? value = element.FindParameter("Door material").AsElementOrDefault(); +Material? value = element.FindParameter("Door material").AsElementOrDefault(); +``` + **Set** sets the parameter to a new value. ```csharp diff --git a/source/Nice3point.Revit.Extensions/ElementIdExtensions.cs b/source/Nice3point.Revit.Extensions/ElementIdExtensions.cs index d56bad8..d0cf46b 100644 --- a/source/Nice3point.Revit.Extensions/ElementIdExtensions.cs +++ b/source/Nice3point.Revit.Extensions/ElementIdExtensions.cs @@ -9,19 +9,49 @@ public static class ElementIdExtensions /// The unique identification for an element. extension(ElementId elementId) { + /// + /// Gets the element referenced by the input ElementId. + /// + /// The document that contains the element. + /// The element. + /// The ElementId references no element in the document. + [Pure] + public Element ToElement(Document document) + { + return document.GetElement(elementId) ?? throw new InvalidOperationException($"The document contains no element with the id {elementId.ToLong()}."); + } + + /// + /// Gets the element of type referenced by the input ElementId. + /// + /// The expected type of the element. + /// The document that contains the element. + /// The element. + /// The ElementId references no element in the document. + /// The element is not of type . + [Pure] + public T ToElement(Document document) where T : Element + { + return (T)elementId.ToElement(document); + } + /// [Pure] - public Element? ToElement(Document document) + public Element? ToElementOrDefault(Document document) { return document.GetElement(elementId); } - /// + /// + /// Gets the element of type referenced by the input ElementId. + /// /// The expected type of the element. + /// The document that contains the element. + /// The element, or when the ElementId references no element or an element of another type. [Pure] - public T? ToElement(Document document) where T : Element + public T? ToElementOrDefault(Document document) where T : Element { - return (T?)document.GetElement(elementId); + return document.GetElement(elementId) as T; } /// diff --git a/source/Nice3point.Revit.Extensions/ParameterExtensions.cs b/source/Nice3point.Revit.Extensions/ParameterExtensions.cs index de3f8cd..ef257c7 100644 --- a/source/Nice3point.Revit.Extensions/ParameterExtensions.cs +++ b/source/Nice3point.Revit.Extensions/ParameterExtensions.cs @@ -45,22 +45,43 @@ public Color AsColor() /// Provides access to the Element within the parameter /// The element contained in the parameter as an ElementId /// The AsElement method should only be used if the StorageType property returns that the internal contents of the parameter is an ElementId + /// The parameter references no element [Pure] - public Element? AsElement() + public Element AsElement() { - var elementId = parameter.AsElementId(); - return elementId == ElementId.InvalidElementId ? null : parameter.Element.Document.GetElement(elementId); + return parameter.AsElementOrDefault() ?? throw new InvalidOperationException($"The parameter '{parameter.Definition.Name}' references no element."); } /// Provides access to the Element within the parameter /// The element contained in the parameter as an ElementId /// The AsElement method should only be used if the StorageType property returns that the internal contents of the parameter is an ElementId /// Type inherited from + /// The parameter references no element + /// The element is not of type + [Pure] + public T AsElement() where T : Element + { + return (T)parameter.AsElement(); + } + + /// Provides access to the Element within the parameter + /// The element contained in the parameter, or when the parameter references no element + /// The AsElementOrDefault method should only be used if the StorageType property returns that the internal contents of the parameter is an ElementId [Pure] - public T? AsElement() where T : Element + public Element? AsElementOrDefault() { var elementId = parameter.AsElementId(); - return elementId == ElementId.InvalidElementId ? null : (T)parameter.Element.Document.GetElement(elementId); + return elementId == ElementId.InvalidElementId ? null : parameter.Element.Document.GetElement(elementId); + } + + /// Provides access to the Element of type within the parameter + /// The element contained in the parameter, or when the parameter references no element or an element of another type + /// The AsElementOrDefault method should only be used if the StorageType property returns that the internal contents of the parameter is an ElementId + /// Type inherited from + [Pure] + public T? AsElementOrDefault() where T : Element + { + return parameter.AsElementOrDefault() as T; } /// Sets the parameter to a new bool value diff --git a/tests/Nice3point.Revit.Extensions.Tests/ElementIdExtensionsTests.cs b/tests/Nice3point.Revit.Extensions.Tests/ElementIdExtensionsTests.cs index 572c32a..c115078 100644 --- a/tests/Nice3point.Revit.Extensions.Tests/ElementIdExtensionsTests.cs +++ b/tests/Nice3point.Revit.Extensions.Tests/ElementIdExtensionsTests.cs @@ -134,19 +134,58 @@ public async Task ToElement_ValidElementId_ReturnsElement(string path) [Test] [MethodDataSource(nameof(RevitFamilies))] - public async Task ToElement_InvalidElementId_ReturnsNull(string path) + public async Task ToElement_InvalidElementId_ThrowsInvalidOperationException(string path) { // Arrange var document = FamilyDocuments[path]; + var invalidId = 999999999L.ToElementId(); -#if REVIT2024_OR_GREATER - var invalidId = new ElementId(999999999L); -#else - var invalidId = new ElementId(999999999); -#endif + // Act & Assert + await Assert.That(() => invalidId.ToElement(document)).Throws(); + } + + [Test] + [MethodDataSource(nameof(RevitFamilies))] + public async Task ToElementGeneric_InvalidElementId_ThrowsInvalidOperationException(string path) + { + // Arrange + var document = FamilyDocuments[path]; + var invalidId = 999999999L.ToElementId(); + + // Act & Assert + await Assert.That(() => invalidId.ToElement(document)).Throws(); + } + + [Test] + [MethodDataSource(nameof(RevitFamilies))] + public async Task ToElementGeneric_DifferentType_ThrowsInvalidCastException(string path) + { + // Arrange + var document = FamilyDocuments[path]; + var instanceId = new FilteredElementCollector(document) + .WhereElementIsNotElementType() + .FirstElementId(); + + if (instanceId == ElementId.InvalidElementId) + { + Skip.Test("No elements found in document"); + return; + } + + // Act & Assert + await Assert.That(() => instanceId.ToElement(document)).Throws(); + } + + [Test] + [MethodDataSource(nameof(RevitFamilies))] + public async Task ToElementOrDefault_InvalidElementId_ReturnsNull(string path) + { + // Arrange + var document = FamilyDocuments[path]; + var invalidId = 999999999L.ToElementId(); // Act - var element = invalidId.ToElement(document); + var element = invalidId.ToElementOrDefault(document); // Assert await Assert.That(element).IsNull(); @@ -180,6 +219,71 @@ public async Task ToElementGeneric_ValidElementId_ReturnsTypedElement(string pat } } + [Test] + [MethodDataSource(nameof(RevitFamilies))] + public async Task ToElementOrDefaultGeneric_MatchingType_ReturnsTypedElement(string path) + { + // Arrange + var document = FamilyDocuments[path]; + var elementTypeId = new FilteredElementCollector(document) + .WhereElementIsElementType() + .FirstElementId(); + + if (elementTypeId == ElementId.InvalidElementId) + { + Skip.Test("No element types found in document"); + return; + } + + // Act + var elementType = elementTypeId.ToElementOrDefault(document); + + // Assert + using (Assert.Multiple()) + { + await Assert.That(elementType).IsNotNull(); + await Assert.That(elementType!.Id).IsEqualTo(elementTypeId); + } + } + + [Test] + [MethodDataSource(nameof(RevitFamilies))] + public async Task ToElementOrDefaultGeneric_DifferentType_ReturnsNull(string path) + { + // Arrange + var document = FamilyDocuments[path]; + var instanceId = new FilteredElementCollector(document) + .WhereElementIsNotElementType() + .FirstElementId(); + + if (instanceId == ElementId.InvalidElementId) + { + Skip.Test("No elements found in document"); + return; + } + + // Act + var elementType = instanceId.ToElementOrDefault(document); + + // Assert + await Assert.That(elementType).IsNull(); + } + + [Test] + [MethodDataSource(nameof(RevitFamilies))] + public async Task ToElementOrDefaultGeneric_InvalidElementId_ReturnsNull(string path) + { + // Arrange + var document = FamilyDocuments[path]; + var invalidId = 999999999L.ToElementId(); + + // Act + var element = invalidId.ToElementOrDefault(document); + + // Assert + await Assert.That(element).IsNull(); + } + [Test] [MethodDataSource(nameof(RevitFamilies))] public async Task ToElements_MultipleElementIds_ReturnsAllElements(string path) diff --git a/tests/Nice3point.Revit.Extensions.Tests/ParameterExtensionsTests.cs b/tests/Nice3point.Revit.Extensions.Tests/ParameterExtensionsTests.cs new file mode 100644 index 0000000..622d636 --- /dev/null +++ b/tests/Nice3point.Revit.Extensions.Tests/ParameterExtensionsTests.cs @@ -0,0 +1,126 @@ +using Nice3point.Revit.Extensions.Tests.Abstractions; + +namespace Nice3point.Revit.Extensions.Tests; + +public sealed class ParameterExtensionsTests : RevitFamilySampleTest +{ + [Test] + [MethodDataSource(nameof(RevitFamilies))] + public async Task AsElement_NoElement_ThrowsInvalidOperationException(string path) + { + // Arrange + var document = FamilyDocuments[path]; + var parameter = FindElementIdParameter(document, static element => element is null); + if (parameter is null) + { + Skip.Test("No parameter references no element in document"); + return; + } + + // Act & Assert + await Assert.That(() => parameter.AsElement()).Throws(); + } + + [Test] + [MethodDataSource(nameof(RevitFamilies))] + public async Task AsElementGeneric_DifferentType_ThrowsInvalidCastException(string path) + { + // Arrange + var document = FamilyDocuments[path]; + var parameter = FindElementIdParameter(document, static element => element is not null and not ElementType); + if (parameter is null) + { + Skip.Test("No parameter references an element instance in document"); + return; + } + + // Act & Assert + await Assert.That(() => parameter.AsElement()).Throws(); + } + + [Test] + [MethodDataSource(nameof(RevitFamilies))] + public async Task AsElementOrDefault_NoElement_ReturnsNull(string path) + { + // Arrange + var document = FamilyDocuments[path]; + var parameter = FindElementIdParameter(document, static element => element is null); + if (parameter is null) + { + Skip.Test("No parameter references no element in document"); + return; + } + + // Act + var element = parameter.AsElementOrDefault(); + + // Assert + await Assert.That(element).IsNull(); + } + + [Test] + [MethodDataSource(nameof(RevitFamilies))] + public async Task AsElementOrDefaultGeneric_MatchingType_ReturnsTypedElement(string path) + { + // Arrange + var document = FamilyDocuments[path]; + var parameter = FindElementIdParameter(document, static element => element is not null and not ElementType); + if (parameter is null) + { + Skip.Test("No parameter references an element instance in document"); + return; + } + + // Act + var element = parameter.AsElementOrDefault(); + + // Assert + using (Assert.Multiple()) + { + await Assert.That(element).IsNotNull(); + await Assert.That(element!.Id).IsEqualTo(parameter.AsElementId()); + } + } + + [Test] + [MethodDataSource(nameof(RevitFamilies))] + public async Task AsElementOrDefaultGeneric_DifferentType_ReturnsNull(string path) + { + // Arrange + var document = FamilyDocuments[path]; + var parameter = FindElementIdParameter(document, static element => element is not null and not ElementType); + if (parameter is null) + { + Skip.Test("No parameter references an element instance in document"); + return; + } + + // Act + var elementType = parameter.AsElementOrDefault(); + + // Assert + await Assert.That(elementType).IsNull(); + } + + private static Parameter? FindElementIdParameter(Document document, Func referencedElementPredicate) + { + var elements = new FilteredElementCollector(document).WhereElementIsNotElementType(); + foreach (var element in elements) + { + foreach (Parameter parameter in element.Parameters) + { + if (parameter.StorageType != StorageType.ElementId) + { + continue; + } + + if (referencedElementPredicate(document.GetElement(parameter.AsElementId()))) + { + return parameter; + } + } + } + + return null; + } +}