Set footings Type using WPF

Hi All, @c.poupin

I’m implementing a pyrevit tool using MVVM (the logic may be similar when using Dynamo) that allows the user to select the desired Footing Type from a combobox dropdown list for each grid intersection point. For now, I’m focusing on assigning a Footing Type Instance to each position corresponding to an intersection point.

As you can see in the example below, the positions (i.e., intersection points) are correctly displayed in the UI, and I can add Footing Type Instances each time I click the Ajouter button. However, when I select a Type Instance for each position from the combobox dropdown lists, an exclamation mark appears for each selection. Additionally, I cannot determine whether those instances have been selected correctly because no message is printed when I click the Appliquer button.

Remark: In the Types comboboxes, I want to display the Name property for each FootingType instance while storing those instances for later use in the code. That’s why I used DisplayMemberPath="Name" in the xaml layout.

Please check my references below:

xaml layout
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Footings"
    Height="Auto" Width="460"
    SizeToContent="Height"
    ResizeMode="NoResize"
    WindowStartupLocation="CenterScreen">
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="5*"/>
            <RowDefinition Height="85*"/>
            <RowDefinition Height="5*"/>
            <RowDefinition Height="5*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50*"/>
            <ColumnDefinition Width="30*"/>
            <ColumnDefinition Width="20*"/>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Column="1" Grid.ColumnSpan="2" Text="Caracteristiques des semelles" FontWeight="Bold" FontSize="11" />
        <DataGrid x:Name="Symbols" Grid.Row="1" Grid.RowSpan="2" AutoGenerateColumns="False"
                Background="Transparent" GridLinesVisibility="None">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Position" Binding="{Binding Position}" Width="*" />
                <DataGridTemplateColumn Header="Type de Semelle" Width="2*">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <ComboBox x:Name="Types" ItemsSource="{Binding LstTypes}"
                                      SelectedItem="{Binding SelectedType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                      DisplayMemberPath="Name">
                            </ComboBox>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid >
        <StackPanel Grid.Row="1" Grid.Column="1">

            <Label Content="Longueur :" VerticalAlignment="Center" Width="75" HorizontalAlignment="Left"/>
            <DockPanel>
                <TextBox x:Name="long_value" Height="20" Width="75" VerticalContentAlignment="Center" Margin="5,0,0,0"/>
                <Label Content="cm"/>
            </DockPanel>
            <Label Content="Largeur :" VerticalAlignment="Center" Width="75" HorizontalAlignment="Left"/>
            <DockPanel>
                <TextBox x:Name="larg_value" Height="20" Width="75" VerticalContentAlignment="Center" Margin="5,0,0,0"/>
                <Label Content="cm"/>
            </DockPanel>
            <Label Content="Epaisseur :" VerticalAlignment="Center" Width="75" HorizontalAlignment="Left"/>
            <DockPanel>
                <TextBox x:Name="ep_value" Height="20" Width="75" VerticalContentAlignment="Center" Margin="5,0,0,0"/>
                <Label Content="cm"/>
            </DockPanel>
        </StackPanel>

        <StackPanel Grid.Row="1" Grid.Column="2" Orientation="Vertical">
            <Button Content="Ajouter" Width="85" Click="Add_Click" VerticalAlignment="Top" Margin="0,30,0,10"/>
            <Button Content="Supprimer" Width="85"  VerticalAlignment="Top" Margin="0,20,0,10"/>
            <Button Content="Supprimer tous" Width="85"  VerticalAlignment="Top" Margin="0,20,0,10"/>
        </StackPanel>
        <StackPanel Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2">
            <Label Content="Contrainte de Béton :" VerticalAlignment="Center"/>
            <DockPanel>
                <TextBox x:Name="contrainte_value" Height="20" Width="75" VerticalContentAlignment="Center" Margin="5,0,0,0"/>
                <Label Content="MPA" />
            </DockPanel>
        </StackPanel>
        <StackPanel Grid.Row="3" Grid.ColumnSpan="3" Orientation="Horizontal" VerticalAlignment="Center" Width="210" Margin="0,5,0,0">
            <Button x:Name="Appliquer" Click="Appliquer_Click" Content="Appliquer" Width="100"/>
            <Button x:Name="Fermer" Click="Fermer_Click"  Content="Fermer" Width="100" Margin="10,0,0,0"/>
        </StackPanel>

    </Grid>
</Window>

Footing_script
# -*- coding: UTF-8 -*-
import wpf, clr, os, itertools, math
from pyrevit import forms, revit

from System.Windows import Window
from System import Array
from System.Collections.ObjectModel import ObservableCollection
from System.ComponentModel import INotifyPropertyChanged, PropertyChangedEventArgs
from System.Windows.Controls import CheckBox, TextBlock, TextBox, ListBoxItem
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import StructuralType  # Add this import

uidoc = __revit__.ActiveUIDocument
doc = __revit__.ActiveUIDocument.Document
app = __revit__.Application
results = clr.Reference[IntersectionResultArray]()


class FootingType:
    def __init__(self, d1, d2, d3):
        self.d1 = d1
        self.d2 = d2
        self.d3 = d3
        if d1 < d2:
            self._dim = [d1, d2, d3]
        else:
            self._dim = [d2, d1, d3]

    @property
    def H(self):
        return self._dim[0]

    @property
    def W(self):
        return self._dim[1]

    @property
    def T(self):
        return self._dim[2]

    @property
    def Name(self):
        return "{}cm x {}cm x {}cm".format(self.W, self.H, self.T)

    def __eq__(self, other):
        return self.H == other.H and self.W == other.W and self.T == other.T

    def __hash__(self):
        return hash(self.Name)

class ViewModelBase(INotifyPropertyChanged):
    
    def __init__(self):
        self._property_changed = None

    def add_PropertyChanged(self, handler):
        self._property_changed = handler

    def remove_PropertyChanged(self, handler):
        self._property_changed = None

    def notify_property_changed(self, property_name):
        if self._property_changed:
            self._property_changed(self, PropertyChangedEventArgs(property_name))

class Footings_VM(ViewModelBase):
    
    def __init__(self, Position):
        self._position = Position
        self._types = ObservableCollection[object]()
        self._selected_type = None

    @property
    def Position(self):
        return self._position

    @Position.setter
    def Position(self, value):
        self._position = value
        self.notify_property_changed('Position')

    @property
    def LstTypes(self):
        return self._types

    @LstTypes.setter
    def LstTypes(self, lst_value):
        self._types = ObservableCollection[object](lst_value)
        self.notify_property_changed('LstTypes')

    @property
    def SelectedType(self):
        return self._selected_type

    @SelectedType.setter
    def SelectedType(self, value):
        self._selected_type = value
        self.notify_property_changed('SelectedType')

    def AddType(self, footing_type):
        self._types.Add(footing_type)
        self.notify_property_changed('LstTypes')

class MyWindow(Window):
    
    def __init__(self):
        script_path = os.path.dirname(__file__)
        xaml_path = os.path.join(script_path, 'footings.xaml')
        wpf.LoadComponent(self, xaml_path)
        self.Data = ObservableCollection[Footings_VM]()
        symbols = self._grids_symbols()
        for s in symbols:
            foot = Footings_VM(s)
            self.Data.Add(foot)
        self.Symbols.ItemsSource = self.Data

    def _grids_symbols(self):
        Symbols = []
        grids = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Grids) \
            .WhereElementIsNotElementType().ToElements()
        for gd_a, gd_b in itertools.combinations(grids, 2):
            curveA = gd_a.Curve
            curveB = gd_b.Curve
            vectA = curveA.ComputeDerivatives(0.5, True).BasisX
            vectB = curveB.ComputeDerivatives(0.5, True).BasisX
            symA = gd_a.Name
            symB = gd_b.Name

            # check if lines are parallel
            if abs(vectA.CrossProduct(vectB).Z) < 0.01:
                # if true go to the next iteration
                continue
            results = clr.Reference[IntersectionResultArray]()
            result = curveA.Intersect(curveB, results)
            if result == SetComparisonResult.Overlap:
                
                if symA.isalpha() and symB.isdigit():
                    symbol = "{}-{}".format(symA, symB)
                elif symA.isdigit() and symB.isalpha():
                    symbol = "{}-{}".format(symB, symA)
                else:
                    continue  

                Symbols.append(symbol)

        return sorted(Symbols)

    def Add_Click(self, sender, e):
        try:
            # Get values from the text boxes
            long_value = float(self.long_value.Text)
            larg_value = float(self.larg_value.Text)
            ep_value = float(self.ep_value.Text)

            # Create a new FootingType instance
            foot = FootingType(long_value, larg_value, ep_value)

            # Add the FootingType instance to all Footings_VM instances in the Data collection
            for vm in self.Data:
                vm.AddType(foot)
                print("Footing Added: {}".format(foot.Name))  # Debugging



            self.long_value.Clear()
            self.larg_value.Clear()
            self.ep_value.Clear()


        except ValueError:
            forms.alert("Please enter valid numeric values for dimensions.")

    def Appliquer_Click(self, sender, e):

        selected_foot = [vm.SelectedType for vm in self.Data if vm.SelectedType is not None]
        if selected_foot:
            print("Selected Footings: {}".format([foot.Name for foot in selected_foot]))
        else:
            print("No footings selected.")
        self.Close()

    def Fermer_Click(self, sender, e):
        self.Close()

def main(doc=None):
    """Main entrypoint for the script."""
    doc = doc or revit.doc
    foundation = MyWindow()
    foundation.ShowDialog()

if __name__ == "__main__":
    main()

How can I fix this issue?

Thanks.

Hi,
I haven’t tested your code, but the first error I see is that you’ve omitted to initialize the super class (inheritance )

class Footings_VM(ViewModelBase):
    
    def __init__(self, Position):
        ViewModelBase.__init__(self) # need to init the super class 
        self._position = Position
        self._types = ObservableCollection[object]()
        self._selected_type = None
1 Like

I corrected this error and updated the code by adding debug points in the Appliquer_Click function. Then, I tested the code by adding two footing types, and it seems to be working for instance addition. However, no footing is selected, as shown in the output!

console output :

Footing Added: 100.0cm x 100.0cm x 20.0cm
Footing Added: 200.0cm x 200.0cm x 20.0cm
No footings selected.
updated code
# -*- coding: UTF-8 -*-
import wpf, clr, os, itertools, math
from pyrevit import forms, revit

from System.Windows import Window
from System import Array
from System.Collections.ObjectModel import ObservableCollection
from System.ComponentModel import INotifyPropertyChanged, PropertyChangedEventArgs
from System.Windows.Controls import CheckBox, TextBlock, TextBox, ListBoxItem
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import StructuralType  # Add this import

uidoc = __revit__.ActiveUIDocument
doc = __revit__.ActiveUIDocument.Document
app = __revit__.Application
results = clr.Reference[IntersectionResultArray]()


class FootingType:
    def __init__(self, d1, d2, d3):
        self.d1 = d1
        self.d2 = d2
        self.d3 = d3
        if d1 < d2:
            self._dim = [d1, d2, d3]
        else:
            self._dim = [d2, d1, d3]

    @property
    def H(self):
        return self._dim[0]

    @property
    def W(self):
        return self._dim[1]

    @property
    def T(self):
        return self._dim[2]

    @property
    def Name(self):
        return "{}cm x {}cm x {}cm".format(self.W, self.H, self.T)

    def __eq__(self, other):
        return self.H == other.H and self.W == other.W and self.T == other.T

    def __hash__(self):
        return hash(self.Name)

class ViewModelBase(INotifyPropertyChanged):

    def __init__(self):
        self._property_changed = None

    def add_PropertyChanged(self, handler):
        self._property_changed = handler

    def remove_PropertyChanged(self, handler):
        self._property_changed = None

    def notify_property_changed(self, property_name):
        if self._property_changed:
            self._property_changed(self, PropertyChangedEventArgs(property_name))

class Footings_VM(ViewModelBase):

    def __init__(self, Position):
        ViewModelBase.__init__(self)
        self._position = Position
        self._types = ObservableCollection[object]()
        self._selected_type = ""

    @property
    def Position(self):
        return self._position

    @Position.setter
    def Position(self, value):
        self._position = value
        self.notify_property_changed('Position')

    @property
    def LstTypes(self):
        return self._types

    @LstTypes.setter
    def LstTypes(self, lst_value):
        self._types = ObservableCollection[object](lst_value)
        self.notify_property_changed('LstTypes')

    @property
    def SelectedType(self):
        return self._selected_type

    @SelectedType.setter
    def SelectedType(self, value):
        self._selected_type = value
        self.notify_property_changed('SelectedType')

    def AddType(self, footing_type):
        self._types.Add(footing_type)
        self.notify_property_changed('LstTypes')

class MyWindow(Window):

    def __init__(self):
        script_path = os.path.dirname(__file__)
        xaml_path = os.path.join(script_path, 'footings.xaml')
        wpf.LoadComponent(self, xaml_path)
        self.Data = ObservableCollection[Footings_VM]()
        symbols = self._grids_symbols()
        for s in symbols:
            foot = Footings_VM(s)
            self.Data.Add(foot)
        self.Symbols.ItemsSource = self.Data

    def _grids_symbols(self):
        Symbols = []
        grids = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Grids) \
            .WhereElementIsNotElementType().ToElements()
        for gd_a, gd_b in itertools.combinations(grids, 2):
            curveA = gd_a.Curve
            curveB = gd_b.Curve
            vectA = curveA.ComputeDerivatives(0.5, True).BasisX
            vectB = curveB.ComputeDerivatives(0.5, True).BasisX
            symA = gd_a.Name
            symB = gd_b.Name

            # check if lines are parallel
            if abs(vectA.CrossProduct(vectB).Z) < 0.01:
                # if true go to the next iteration
                continue
            results = clr.Reference[IntersectionResultArray]()
            result = curveA.Intersect(curveB, results)
            if result == SetComparisonResult.Overlap:

                if symA.isalpha() and symB.isdigit():
                    symbol = "{}-{}".format(symA, symB)
                elif symA.isdigit() and symB.isalpha():
                    symbol = "{}-{}".format(symB, symA)
                else:
                    continue

                Symbols.append(symbol)

        return sorted(Symbols)

    def Add_Click(self, sender, e):
        try:
            # Get values from the text boxes
            long_value = float(self.long_value.Text)
            larg_value = float(self.larg_value.Text)
            ep_value = float(self.ep_value.Text)

            # Create a new FootingType instance
            foot = FootingType(long_value, larg_value, ep_value)

            # Add the FootingType instance to all Footings_VM instances in the Data collection
            for vm in self.Data:
                vm.AddType(foot)

            print("Footing Added: {}".format(foot.Name))  # Debugging



            self.long_value.Clear()
            self.larg_value.Clear()
            self.ep_value.Clear()


        except ValueError:
            forms.alert("Please enter valid numeric values for dimensions.")

    def Appliquer_Click(self, sender, e):
        selected_foot = [vm.SelectedType for vm in self.Data if isinstance(vm.SelectedType, FootingType)]
        if selected_foot:
            print("Selected Footings: {}".format([foot.Name for foot in selected_foot]))
        else:
            print("No footings selected.")
        self.Close()

    def Fermer_Click(self, sender, e):
        self.Close()

def main(doc=None):
    """Main entrypoint for the script."""
    doc = doc or revit.doc
    foundation = MyWindow()
    foundation.ShowDialog()

if __name__ == "__main__":
    main()```

Thanks.

self._selected_type must be same type of self._types ( ObservableCollection[FootingType] ), so at initialization self._selected_type can’t be a string

a workaround is to create a dummy object

code IronPython3 for Dynamo tested on Revit 2025

there’s probably still room for improvement

import clr  
import sys
import System
from System import Array
from System.Collections.Generic import List, KeyValuePair
from System.Collections.ObjectModel import ObservableCollection

#import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument

clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)

clr.AddReference("IronPython.Wpf")
clr.AddReference('System.Core')
clr.AddReference('System.Xml')
clr.AddReference('PresentationCore')
clr.AddReference('PresentationFramework')
#clr.AddReferenceByPartialName("WindowsBase")

from System.IO import StringReader
from System.Windows.Markup import XamlReader, XamlWriter
from System.Windows import Window, Application
from System.ComponentModel import INotifyPropertyChanged
from System.ComponentModel import PropertyChangedEventArgs
from System.Windows import Window
from System.ComponentModel import INotifyPropertyChanged, PropertyChangedEventArgs
#from System.Windows.Controls import CheckBox, TextBlock, TextBox, ListBoxItem

import wpf
import itertools
import traceback

class FootingType(System.Object):
    def __init__(self, d1, d2, d3):
        self.d1 = d1
        self.d2 = d2
        self.d3 = d3
        if d1 < d2:
            self._dim = [d1, d2, d3]
        else:
            self._dim = [d2, d1, d3]

    @property
    def H(self):
        return self._dim[0]

    @property
    def W(self):
        return self._dim[1]

    @property
    def T(self):
        return self._dim[2]

    @property
    def Name(self):
        if 0 == self.W == self.H == self.T:
            return "??"
        else:
            return "{}cm x {}cm x {}cm".format(self.W, self.H, self.T)

    def __eq__(self, other):
        return self.H == other.H and self.W == other.W and self.T == other.T

    def __hash__(self):
        return hash(self.Name)

class ViewModelBase(INotifyPropertyChanged):

    def __init__(self):
        self.propertyChangedHandlers = []
    #
    # define a method to raise the PropertyChanged event
    def RaisePropertyChanged(self, propertyName):
        # create a PropertyChangedEventArgs object with the name of the changed property
        args = PropertyChangedEventArgs(propertyName)
        for handler in self.propertyChangedHandlers:
            # invoke each of the registered property changed handlers with the ViewModelBase instance and the event arguments
            handler(self, args)
    #
    # define a method to add a property changed handler
    def add_PropertyChanged(self, handler):
        self.propertyChangedHandlers.append(handler)
    #
    # define a method to remove a property changed handler
    def remove_PropertyChanged(self, handler):
        self.propertyChangedHandlers.remove(handler)
        
        
class Footings_VM(ViewModelBase):

    def __init__(self, Position):
        ViewModelBase.__init__(self)
        self._position = Position
        self._types = ObservableCollection[FootingType]() 
        self._types.Add(FootingType(0, 0, 0)) # create dummy object 
        self._selected_type = self._types[0] # assign the dummy object at initialisation 

    @property
    def Position(self):
        return self._position

    @Position.setter
    def Position(self, value):
        self._position = value
        self.RaisePropertyChanged('Position')

    @property
    def LstTypes(self):
        return self._types

    @LstTypes.setter
    def LstTypes(self, lst_value):
        self._types = ObservableCollection[object](lst_value)
        self.RaisePropertyChanged('LstTypes')

    @property
    def SelectedType(self):
        return self._selected_type

    @SelectedType.setter
    def SelectedType(self, value):
        self._selected_type = value
        self.RaisePropertyChanged('SelectedType')

    def AddType(self, footing_type):
        self._types.Add(footing_type)
        self.RaisePropertyChanged('LstTypes')

class MyWindow(Window):
    
    xaml_str = '''
    <Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Footings"
        Height="Auto" Width="460"
        SizeToContent="Height"
        ResizeMode="NoResize"
        WindowStartupLocation="CenterScreen">
        <Grid Margin="10">
            <Grid.RowDefinitions>
                <RowDefinition Height="5*"/>
                <RowDefinition Height="85*"/>
                <RowDefinition Height="5*"/>
                <RowDefinition Height="5*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="50*"/>
                <ColumnDefinition Width="30*"/>
                <ColumnDefinition Width="20*"/>
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="1" Grid.ColumnSpan="2" Text="Caracteristiques des semelles" FontWeight="Bold" FontSize="11" />
            <DataGrid x:Name="Symbols" Grid.Row="1" Grid.RowSpan="2" AutoGenerateColumns="False" ItemsSource="{Binding}"
                    Background="Transparent" GridLinesVisibility="None">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Position" Binding="{Binding Position}" Width="*" />
                    <DataGridTemplateColumn Header="Type de Semelle" Width="2*">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <ComboBox x:Name="Types" 
                                          ItemsSource="{Binding LstTypes}"
                                          SelectedItem="{Binding SelectedType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                          DisplayMemberPath="Name">
                                </ComboBox>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid >
            <StackPanel Grid.Row="1" Grid.Column="1">
    
                <Label Content="Longueur :" VerticalAlignment="Center" Width="75" HorizontalAlignment="Left"/>
                <DockPanel>
                    <TextBox x:Name="long_value" Height="20" Width="75" VerticalContentAlignment="Center" Margin="5,0,0,0"/>
                    <Label Content="cm"/>
                </DockPanel>
                <Label Content="Largeur :" VerticalAlignment="Center" Width="75" HorizontalAlignment="Left"/>
                <DockPanel>
                    <TextBox x:Name="larg_value" Height="20" Width="75" VerticalContentAlignment="Center" Margin="5,0,0,0"/>
                    <Label Content="cm"/>
                </DockPanel>
                <Label Content="Epaisseur :" VerticalAlignment="Center" Width="75" HorizontalAlignment="Left"/>
                <DockPanel>
                    <TextBox x:Name="ep_value" Height="20" Width="75" VerticalContentAlignment="Center" Margin="5,0,0,0"/>
                    <Label Content="cm"/>
                </DockPanel>
            </StackPanel>
    
            <StackPanel Grid.Row="1" Grid.Column="2" Orientation="Vertical">
                <Button Content="Ajouter" Width="85" Click="Add_Click" VerticalAlignment="Top" Margin="0,30,0,10"/>
                <Button Content="Supprimer" Width="85"  VerticalAlignment="Top" Margin="0,20,0,10"/>
                <Button Content="Supprimer tous" Width="85"  VerticalAlignment="Top" Margin="0,20,0,10"/>
            </StackPanel>
            <StackPanel Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2">
                <Label Content="Contrainte de Béton :" VerticalAlignment="Center"/>
                <DockPanel>
                    <TextBox x:Name="contrainte_value" Height="20" Width="75" VerticalContentAlignment="Center" Margin="5,0,0,0"/>
                    <Label Content="MPA" />
                </DockPanel>
            </StackPanel>
            <StackPanel Grid.Row="3" Grid.ColumnSpan="3" Orientation="Horizontal" VerticalAlignment="Center" Width="210" Margin="0,5,0,0">
                <Button x:Name="Appliquer" Click="Appliquer_Click" Content="Appliquer" Width="100"/>
                <Button x:Name="Fermer" Click="Fermer_Click"  Content="Fermer" Width="100" Margin="10,0,0,0"/>
            </StackPanel>
    
        </Grid>
    </Window>
    '''

    def __init__(self):
        wpf.LoadComponent(self, StringReader(MyWindow.xaml_str))
        self.xData = ObservableCollection[Footings_VM]()
        symbols = self._grids_symbols()
        print(symbols)
        for s in symbols:
            foot = Footings_VM(s)
            self.xData.Add(foot)
        #self.Symbols.ItemsSource = self.Data
        self.DataContext = self.xData

    def _grids_symbols(self):
        Symbols = []
        grids = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Grids) \
            .WhereElementIsNotElementType().ToElements()
        for gd_a, gd_b in itertools.combinations(grids, 2):
            curveA = gd_a.Curve
            curveB = gd_b.Curve
            vectA = curveA.ComputeDerivatives(0.5, True).BasisX
            vectB = curveB.ComputeDerivatives(0.5, True).BasisX
            symA = gd_a.Name
            symB = gd_b.Name

            # check if lines are parallel
            if abs(vectA.CrossProduct(vectB).Z) < 0.01:
                # if true go to the next iteration
                continue
            results = clr.Reference[IntersectionResultArray]()
            result = curveA.Intersect(curveB, results)
            if result == SetComparisonResult.Overlap:

                if symA.isalpha() and symB.isdigit():
                    symbol = "{}-{}".format(symA, symB)
                elif symA.isdigit() and symB.isalpha():
                    symbol = "{}-{}".format(symB, symA)
                else:
                    continue

                Symbols.append(symbol)

        return sorted(Symbols)

    def Add_Click(self, sender, e):
        try:
            # Get values from the text boxes
            long_value = float(self.long_value.Text)
            larg_value = float(self.larg_value.Text)
            ep_value = float(self.ep_value.Text)

            # Create a new FootingType instance
            foot = FootingType(long_value, larg_value, ep_value)

            # Add the FootingType instance to all Footings_VM instances in the Data collection
            for vm in self.xData:
                vm.AddType(foot)

            print("Footing Added: {}".format(foot.Name))  # Debugging
            self.long_value.Clear()
            self.larg_value.Clear()
            self.ep_value.Clear()
        except Exception as ex:
            print(traceback.format_exc())

    def Appliquer_Click(self, sender, e):

        selected_foot = [[vm.Position, vm.SelectedType.Name] for vm in self.xData if vm.SelectedType.Name != "??"]
            
        if selected_foot:
            print(f"Selected Footings: {selected_foot}")
        else:
            print("No footings selected.")
        self.Close()

    def Fermer_Click(self, sender, e):
        self.Close()

foundation = MyWindow()
foundation.ShowDialog()
3 Likes

@c.poupin

Sorry for the late feedback.

As usual, you have the magic trick that reveals the issue! If I had spent hours on this, I wouldn’t have had the intuition to use a dummy object.
The issue is now resolved by implementing a dummy object, just as you suggested.

Now, I’m wondering if I can use WPF’s RelayCommand to handle the Ajouter, Supprimer, and Supprimer tous button actions to improve the code. What do you think?

I would only implement the clear method, avoiding deleting the dummy object

you’ll have to reset the comboboxes to their default values

Note
It might be a good exercise to port this code to PythonNet3

2 Likes

@c.poupin

After a few days’ break, I was finally able to implement the del_Click event, which allows me to delete the selected FootingType instance from the combobox dropdown list. However, for now, it’s still difficult for me to implement delAll_Click, which would allow deleting all FootingType instances.

here the final code:
Note: It is necessary to add the path for the lib subfolder within the attached Footings folder for the code to work as expected.

Footings.7z (1.5 KB)

Footings
import clr  
import sys
sys.path.append(r"C:\Users\nono\OneDrive\Desktop\Footings\lib")
import System
import math
from System import Array
from System.Collections.Generic import List, KeyValuePair
from System.Collections.ObjectModel import ObservableCollection

# Import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB
from Autodesk.Revit.DB.Structure import StructuralType

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument

clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)

clr.AddReference("IronPython.Wpf")
clr.AddReference('System.Core')
clr.AddReference('System.Xml')
clr.AddReference('PresentationCore')
clr.AddReference('PresentationFramework')
clr.AddReference('System.Windows.Forms')  # For MessageBox

from System.IO import StringReader
from System.Windows.Markup import XamlReader, XamlWriter
from System.Windows import Window, Application
from System.ComponentModel import INotifyPropertyChanged
from System.ComponentModel import PropertyChangedEventArgs
from System.Windows import Window
from System.Windows.Forms import MessageBox

import wpf
import itertools
import traceback

from _mySymbols import FootingType
from _myMaterials import ConcreteMaterial, get_or_create_concrete_material


class ViewModelBase(INotifyPropertyChanged):
    def __init__(self):
        self.propertyChangedHandlers = []

    def RaisePropertyChanged(self, propertyName):
        args = PropertyChangedEventArgs(propertyName)
        for handler in self.propertyChangedHandlers:
            handler(self, args)

    def add_PropertyChanged(self, handler):
        self.propertyChangedHandlers.append(handler)

    def remove_PropertyChanged(self, handler):
        self.propertyChangedHandlers.remove(handler)
        
class Footings_VM(ViewModelBase):
    def __init__(self, Position):
        ViewModelBase.__init__(self)
        self._position = Position
        self._types = ObservableCollection[FootingType]() 
        self._types.Add(FootingType(0, 0, 0))
        self._selected_type = self._types[0]

    @property
    def Position(self):
        return self._position

    @Position.setter
    def Position(self, value):
        self._position = value
        self.RaisePropertyChanged('Position')

    @property
    def LstTypes(self):
        return self._types

    @LstTypes.setter
    def LstTypes(self, lst_value):
        self._types = ObservableCollection[object](lst_value)
        self.RaisePropertyChanged('LstTypes')

    @property
    def SelectedType(self):
        return self._selected_type

    @SelectedType.setter
    def SelectedType(self, value):
        self._selected_type = value
        self.RaisePropertyChanged('SelectedType')

    def AddType(self, footing_type):
        self._types.Add(footing_type)
        self.RaisePropertyChanged('LstTypes')

def _grids_symbols():
    symbols = []
    grids = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Grids) \
        .WhereElementIsNotElementType().ToElements()

    for gd_a, gd_b in itertools.combinations(grids, 2):
        symA = gd_a.Name
        symB = gd_b.Name
        if gd_a.Curve.Intersect(gd_b.Curve) == SetComparisonResult.Overlap:
            if symA.isalpha() and symB.isdigit():
                symbol = "{}-{}".format(symA, symB)
                symbols.append(symbol)
            else:
                symbol = "{}-{}".format(symB, symA)
                symbols.append(symbol)
                
    return sorted(symbols)

class MyWindow(Window):
    xaml_str = '''
    <Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Footings"
        Height="Auto" Width="460"
        SizeToContent="Height"
        ResizeMode="NoResize"
        WindowStartupLocation="CenterScreen">
        <Grid Margin="10">
            <Grid.RowDefinitions>
                <RowDefinition Height="5*"/>
                <RowDefinition Height="85*"/>
                <RowDefinition Height="5*"/>
                <RowDefinition Height="5*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="50*"/>
                <ColumnDefinition Width="30*"/>
                <ColumnDefinition Width="20*"/>
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="1" Grid.ColumnSpan="2" Text="Caracteristiques des semelles" FontWeight="Bold" FontSize="11" />
            <DataGrid x:Name="Symbols" Grid.Row="1" Grid.RowSpan="2" AutoGenerateColumns="False" ItemsSource="{Binding}"
                    Background="Transparent" GridLinesVisibility="None">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Position" Binding="{Binding Position}" Width="*" />
                    <DataGridTemplateColumn Header="Type de Semelle" Width="2*">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <ComboBox x:Name="Types"
                                          ItemsSource="{Binding LstTypes}"
                                          SelectedItem="{Binding SelectedType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                          SelectionChanged="Types_SelectionChanged"
                                          DisplayMemberPath="Name">
                                </ComboBox>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid >
            <StackPanel Grid.Row="1" Grid.Column="1">
                <Label Content="Longueur :" VerticalAlignment="Center" Width="75" HorizontalAlignment="Left"/>
                <DockPanel>
                    <TextBox x:Name="long_value" Height="20" Width="75" VerticalContentAlignment="Center" Margin="5,0,0,0"/>
                    <Label Content="cm"/>
                </DockPanel>
                <Label Content="Largeur :" VerticalAlignment="Center" Width="75" HorizontalAlignment="Left"/>
                <DockPanel>
                    <TextBox x:Name="larg_value" Height="20" Width="75" VerticalContentAlignment="Center" Margin="5,0,0,0"/>
                    <Label Content="cm"/>
                </DockPanel>
                <Label Content="Epaisseur :" VerticalAlignment="Center" Width="75" HorizontalAlignment="Left"/>
                <DockPanel>
                    <TextBox x:Name="ep_value" Height="20" Width="75" VerticalContentAlignment="Center" Margin="5,0,0,0"/>
                    <Label Content="cm"/>
                </DockPanel>
            </StackPanel>
            <StackPanel Grid.Row="1" Grid.Column="2" Orientation="Vertical">
                <Button Content="Ajouter" Width="85" Click="Add_Click" VerticalAlignment="Top" Margin="0,30,0,10"/>
                <Button Content="Supprimer" Width="85" Click="del_Click" VerticalAlignment="Top" Margin="0,20,0,10"/>
                <Button Content="Supprimer tous" Width="85" VerticalAlignment="Top" Margin="0,20,0,10"/>
            </StackPanel>
            <StackPanel Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2">
                <Label Content="Contrainte de Béton :" VerticalAlignment="Center"/>
                <DockPanel>
                    <TextBox x:Name="contrainte_value" Height="20" Width="75" VerticalContentAlignment="Center" Margin="5,0,0,0"/>
                    <Label Content="MPA" />
                </DockPanel>
            </StackPanel>
            <StackPanel Grid.Row="3" Grid.ColumnSpan="3" Orientation="Horizontal" VerticalAlignment="Center" Width="210" Margin="0,5,0,0">
                <Button x:Name="Appliquer" Click="Appliquer_Click" Content="Appliquer" Width="100"/>
                <Button x:Name="Fermer" Click="Fermer_Click" Content="Fermer" Width="100" Margin="10,0,0,0"/>
            </StackPanel>
        </Grid>
    </Window>
    '''

    def __init__(self):
        wpf.LoadComponent(self, StringReader(MyWindow.xaml_str))
        self.Data = {}
        self.DataTypes = ObservableCollection[Footings_VM]()
        self.symbols = _grids_symbols()
        for s in self.symbols:
            foot = Footings_VM(s)
            self.DataTypes.Add(foot)
        self.DataContext = self.DataTypes
        self.selected_position = None
                
    def Types_SelectionChanged(self, sender, e):
        if sender.SelectedItem is not None:
            self.selected_position = sender.SelectedItem

    def Add_Click(self, sender, e):
        try:
            long_value = float(self.long_value.Text)
            larg_value = float(self.larg_value.Text)
            ep_value = float(self.ep_value.Text)

            foot = FootingType(long_value, larg_value, ep_value)
            for vm in self.DataTypes:
                vm.AddType(foot)

            self.long_value.Clear()
            self.larg_value.Clear()
            self.ep_value.Clear()

        except ValueError:
            MessageBox.Show("Please enter valid numeric values for dimensions.", "Error")

    def del_Click(self, sender, e):
        
        if self.selected_position is not None:
            index_to_remove = None
            
            for item in self.DataTypes:
                if self.selected_position in item.LstTypes:
                    index_to_remove = item.LstTypes.IndexOf(self.selected_position)
                    break

            if index_to_remove is not None:
                for item in self.DataTypes:
                    if index_to_remove < len(item.LstTypes):
                        item.LstTypes.RemoveAt(index_to_remove)

                        if len(item.LstTypes) == 0:
                            default_type = FootingType(0, 0, 0)
                            item.LstTypes.Add(default_type)
                            item.SelectedType = default_type
                        elif item.SelectedType is None or item.SelectedType == self.selected_position:
                            item.SelectedType = item.LstTypes[0]

        
            self.selected_position = None

    def _collect_input_data(self):
        for item in self.DataTypes:
            if len(item.LstTypes) < 2:
                MessageBox.Show("Please add at least one footing type", "Error")
                return False
            elif item.SelectedType is None or item.SelectedType.H == 0:
                MessageBox.Show("Please select a footing type for each Position", "Error")
                return False

        if not self.contrainte_value.Text:
            MessageBox.Show("Please enter a valid value for : contrainte", "Error")
            return False

        try:
            self.Data["foot"] = [vm.SelectedType for vm in self.DataTypes if vm.SelectedType.Name != "??"]
            self.Data["contrainte"] = self.contrainte_value.Text
            return True
        except ValueError:
            MessageBox.Show("Invalid input detected. Please ensure all inputs are numeric where required.", "Error")
            return False

    def Appliquer_Click(self, sender, e):
        if not self._collect_input_data():
            return
        if not self.Data:
            return
        self.Data["lvl"] = get_foundation_level()
        self.Close()

    def Fermer_Click(self, sender, e):
        self.Close()

def get_foundation_level():
    levels = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Levels) \
            .WhereElementIsNotElementType().ToElements()
    return min(levels, key=lambda l: l.Elevation)

def get_grid_intersection_points(grids):
    lstPts = []
    results = clr.Reference[IntersectionResultArray]()
    for gd_a, gd_b in itertools.combinations(grids, 2):
        curveA = gd_a.Curve
        curveB = gd_b.Curve
        vectA = curveA.ComputeDerivatives(0.5, True).BasisX
        vectB = curveB.ComputeDerivatives(0.5, True).BasisX

        if abs(vectA.CrossProduct(vectB).Z) < 0.01:
            continue
            
        result = curveA.Intersect(curveB, results)
        if result == SetComparisonResult.Overlap:
            interResult = results.Value
            lstPts.append(interResult[0].XYZPoint)
            lstPts.sort(key=lambda p: (p.Y, p.X))
    return lstPts

def CreateFootingTypes(LstTypes, doc):
    all = list(LstTypes)
    created_types = []
    symbols = FilteredElementCollector(doc) \
            .WhereElementIsElementType() \
            .OfCategory(BuiltInCategory.OST_StructuralFoundation) \
            .ToElements()

    footing_names = {"Base rectangulaire", "M_Base-Rectangulaire"}
    existing_types_dict = {Element.Name.GetValue(fs): fs for fs in symbols if fs.FamilyName in footing_names}
    created_types_dict = {}

    with Transaction(doc, "Create Footing Types") as t:
        t.Start()
        for ft in all:
            if ft.Name in existing_types_dict:
                created_types.append(existing_types_dict[ft.Name])
            elif ft.Name in created_types_dict:
                created_types.append(created_types_dict[ft.Name])
            else:
                first_type = next(iter(existing_types_dict.values()), None)
                if not first_type:
                    MessageBox.Show("No existing footing family found.", "Error")
                    return None

                new_type = first_type.Duplicate(ft.Name)
                new_type.LookupParameter("Largeur").Set(ft.H / 30.48)
                new_type.LookupParameter("Longueur").Set(ft.W / 30.48)
                new_type.LookupParameter("Epaisseur de fondation").Set(ft.T / 30.48)
                created_types.append(new_type)
                created_types_dict[ft.Name] = new_type
        t.Commit()
    return created_types

def Create_footings(points, family_symbol, level, material_id, doc):
    with Transaction(doc, "Create Footings") as t:
        t.Start()
        for p, s in zip(points, family_symbol):
            foot = doc.Create.NewFamilyInstance(p, s, level, StructuralType.Footing)
            material_param = foot.get_Parameter(BuiltInParameter.STRUCTURAL_MATERIAL_PARAM)
            if material_param:
                material_param.Set(material_id)
        t.Commit()

# Main execution
grids = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Grids) \
        .WhereElementIsNotElementType().ToElements()

foundation = MyWindow()
foundation.ShowDialog()

Data = foundation.Data
Data = foundation.Data
family_symbol = CreateFootingTypes(Data["foot"], doc)
concrete_material = ConcreteMaterial(Data["contrainte"])
material_id = get_or_create_concrete_material(concrete_material, doc)
    
if family_symbol:
    points = get_grid_intersection_points(grids)
    Create_footings(points, family_symbol, Data["lvl"], material_id, doc)
    
OUT = 0

Thanks.

1 Like