Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` | `InvalidOperationException` | `InvalidCastException` |
| `AsElement`, `AsElement<T>` | `InvalidOperationException` | `InvalidCastException` |
| `ToElementOrDefault`, `ToElementOrDefault<T>` | `null` | `null` |
| `AsElementOrDefault`, `AsElementOrDefault<T>` | `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<View>(document);
if (view is null)
{
return;
}
```

- `elementId.ToElementOrDefault(Document)`
- `elementId.ToElementOrDefault<T>(Document)`
- `parameter.AsElementOrDefault()`
- `parameter.AsElementOrDefault<T>()`

## Breaking changes

**`ToElement` and `AsElement` throw for a missing element.**
`ToElement`, `ToElement<T>`, `AsElement`, and `AsElement<T>` 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<Level>();
if (level is null) return;

// After
var level = parameter.AsElementOrDefault<Level>();
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.
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Wall>(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<View>(document);
```

**ToElements** retrieves a collection of elements associated with the specified ElementIds.

```csharp
Expand Down Expand Up @@ -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<Material>();
```

**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<Material>();
```

**Set** sets the parameter to a new value.

```csharp
Expand Down
38 changes: 34 additions & 4 deletions source/Nice3point.Revit.Extensions/ElementIdExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,49 @@ public static class ElementIdExtensions
/// <param name="elementId">The unique identification for an element.</param>
extension(ElementId elementId)
{
/// <summary>
/// Gets the element referenced by the input ElementId.
/// </summary>
/// <param name="document">The document that contains the element.</param>
/// <returns>The element.</returns>
/// <exception cref="System.InvalidOperationException">The ElementId references no element in the document.</exception>
[Pure]
public Element ToElement(Document document)
{
return document.GetElement(elementId) ?? throw new InvalidOperationException($"The document contains no element with the id {elementId.ToLong()}.");
}

/// <summary>
/// Gets the element of type <typeparamref name="T" /> referenced by the input ElementId.
/// </summary>
/// <typeparam name="T">The expected type of the element.</typeparam>
/// <param name="document">The document that contains the element.</param>
/// <returns>The element.</returns>
/// <exception cref="System.InvalidOperationException">The ElementId references no element in the document.</exception>
/// <exception cref="System.InvalidCastException">The element is not of type <typeparamref name="T" />.</exception>
[Pure]
public T ToElement<T>(Document document) where T : Element
{
return (T)elementId.ToElement(document);
}

/// <inheritdoc cref="Autodesk.Revit.DB.Document.GetElement(Autodesk.Revit.DB.ElementId)" />
[Pure]
public Element? ToElement(Document document)
public Element? ToElementOrDefault(Document document)
{
return document.GetElement(elementId);
}

/// <inheritdoc cref="Autodesk.Revit.DB.Document.GetElement(Autodesk.Revit.DB.ElementId)" />
/// <summary>
/// Gets the element of type <typeparamref name="T" /> referenced by the input ElementId.
/// </summary>
/// <typeparam name="T">The expected type of the element.</typeparam>
/// <param name="document">The document that contains the element.</param>
/// <returns>The element, or <see langword="null" /> when the ElementId references no element or an element of another type.</returns>
[Pure]
public T? ToElement<T>(Document document) where T : Element
public T? ToElementOrDefault<T>(Document document) where T : Element
{
return (T?)document.GetElement(elementId);
return document.GetElement(elementId) as T;
}

/// <summary>
Expand Down
31 changes: 26 additions & 5 deletions source/Nice3point.Revit.Extensions/ParameterExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,43 @@ public Color AsColor()
/// <summary>Provides access to the Element within the parameter</summary>
/// <returns>The element contained in the parameter as an ElementId</returns>
/// <remarks>The AsElement method should only be used if the StorageType property returns that the internal contents of the parameter is an ElementId</remarks>
/// <exception cref="System.InvalidOperationException">The parameter references no element</exception>
[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.");
}

/// <summary>Provides access to the Element within the parameter</summary>
/// <returns>The element contained in the parameter as an ElementId</returns>
/// <remarks>The AsElement method should only be used if the StorageType property returns that the internal contents of the parameter is an ElementId</remarks>
/// <typeparam name="T">Type inherited from <see cref="Autodesk.Revit.DB.Element" /></typeparam>
/// <exception cref="System.InvalidOperationException">The parameter references no element</exception>
/// <exception cref="System.InvalidCastException">The element is not of type <typeparamref name="T" /></exception>
[Pure]
public T AsElement<T>() where T : Element
{
return (T)parameter.AsElement();
}

/// <summary>Provides access to the Element within the parameter</summary>
/// <returns>The element contained in the parameter, or <see langword="null" /> when the parameter references no element</returns>
/// <remarks>The AsElementOrDefault method should only be used if the StorageType property returns that the internal contents of the parameter is an ElementId</remarks>
[Pure]
public T? AsElement<T>() 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);
}

/// <summary>Provides access to the Element of type <typeparamref name="T" /> within the parameter</summary>
/// <returns>The element contained in the parameter, or <see langword="null" /> when the parameter references no element or an element of another type</returns>
/// <remarks>The AsElementOrDefault method should only be used if the StorageType property returns that the internal contents of the parameter is an ElementId</remarks>
/// <typeparam name="T">Type inherited from <see cref="Autodesk.Revit.DB.Element" /></typeparam>
[Pure]
public T? AsElementOrDefault<T>() where T : Element
{
return parameter.AsElementOrDefault() as T;
}

/// <summary>Sets the parameter to a new bool value</summary>
Expand Down
118 changes: 111 additions & 7 deletions tests/Nice3point.Revit.Extensions.Tests/ElementIdExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<InvalidOperationException>();
}

[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<Element>(document)).Throws<InvalidOperationException>();
}

[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<ElementType>(document)).Throws<InvalidCastException>();
}

[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();
Expand Down Expand Up @@ -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<ElementType>(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<ElementType>(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<Element>(document);

// Assert
await Assert.That(element).IsNull();
}

[Test]
[MethodDataSource(nameof(RevitFamilies))]
public async Task ToElements_MultipleElementIds_ReturnsAllElements(string path)
Expand Down
Loading