Event Handlers to change selection from Combobox

Hi All, @c.poupin

I’m implementing a UI using WPF to create grids in Revit. To allow users to switch between numeric values (‘1, 2, 3, …’) and alphabetic values (‘A, B, C, …’) for grid names, I used a ComboBox, as shown below.

However, I’m struggling to set up event handlers to manage the selection for each grid orientation (X and Y) and to prevent the user from selecting the same values for both X and Y.

Grid script + xaml layout
import clr    
import sys
import System

clr.AddReference("IronPython.Wpf")
clr.AddReference("PresentationFramework")
clr.AddReference("System.Xml")
clr.AddReference("PresentationCore")
clr.AddReference("System.Windows")

from System.IO import StringReader
from System.Windows.Markup import XamlReader, XamlWriter
from System.Windows import Window, Application
import System.Windows.Controls 
from System.Windows.Controls import StackPanel, DockPanel, TextBlock, TextBox, Button, Separator
import wpf

#import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB
clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)
clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument



class Grids(Window):
    LAYOUT = '''
        <Window 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Grids"
            Height="Auto"
            Width="300"
            SizeToContent ="Height" 
            ResizeMode="NoResize"
            WindowStartupLocation="CenterScreen">
            <StackPanel Margin="10">
                <!--Input LX-->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Longueur Total suivant X (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   Lx =" Width="50" Height="25" Margin="0,0,5,0" RenderTransformOrigin="1.733,0.495" />
                        <TextBox x:Name="input_Lx" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Input LY-->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Longueur Total suivant Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   Ly =" Width="50" Height="25" Margin="0,0,5,0" RenderTransformOrigin="1.733,0.495" />
                        <TextBox x:Name="input_Ly" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Grid Extension -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Extension des Axes X, Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   e =" Width="50" Height="25" Margin="0,0,5,0" RenderTransformOrigin="1.733,0.495" />
                        <TextBox x:Name="input_extension" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Spacing along X axis -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Espacement suivant X (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   dx =" Width="50" Height="25" Margin="0,0,5,0" RenderTransformOrigin="1.733,0.495" />
                        <TextBox x:Name="input_dx" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Spacing along Y axis -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Espacement suivant Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   dy =" Width="50" Height="25" Margin="0,0,5,0" RenderTransformOrigin="1.733,0.495" />
                        <TextBox x:Name="input_dy" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--bubbles symbol -->
                <StackPanel>
                    <TextBlock Text="Choix de symbols pour Lx, Ly:" FontWeight="Bold"/>
                    <!-- Symbol Type for Lx -->
                    <DockPanel Margin="0,0,0,10">
                        <TextBlock Text="Type de symbole Lx:" Width="120"/>
                        <ComboBox x:Name="dropdown_symbol_x" Width="150" HorizontalAlignment="Right" >
                            <ComboBoxItem Content="1 2 3 ..." />
                            <ComboBoxItem Content="A B C ..." />
                        </ComboBox>
                    </DockPanel>
                
                    <!-- Symbol Type for Ly -->
                    <DockPanel Margin="0,0,0,10">
                        <TextBlock Text="Type de symbole Ly:" Width="120"/>
                        <ComboBox x:Name="dropdown_symbol_y" Width="150" HorizontalAlignment="Right" >
                            <ComboBoxItem Content="1 2 3 ..." />
                            <ComboBoxItem Content="A B C ..." />
                        </ComboBox>
                    </DockPanel>
                
                </StackPanel>
                
                <!--Separator, OK and Cancel Button-->
                <Separator Margin="0,0,0,10"/>
                <DockPanel HorizontalAlignment="Center">
                    <Button Content="OK" Width="75" Height="25" Margin="0,0,10,0" Click="OK_Clicked" />
                    <Button Content="Cancel" Width="75" Height="25" Click="Cancel_Clicked" />
                </DockPanel>            
            </StackPanel>
        </Window>'''
    def __init__(self):
        wpf.LoadComponent(self, StringReader(Grids.LAYOUT))
        # Symbol sets
        self.all_symbols_numeric = ['1', '2', '3', '4', '5']  # Numeric
        self.all_symbols_alpha = ['A', 'B', 'C', 'D', 'E']  # Alphabetic
        """I'm struggling with how to handle the event triggered 
by a user selection to correctly populate the symbols Sx and Sy in self.data 
while preventing the user from selecting the same symbols for both Sx and Sy"""
        
        self.selected_symbol_x = None
        self.selected_symbol_y = None
        self.data = {}
        self.ShowDialog()

    def _collect_input_data(self):
        """Collect and convert input data from the UI"""
        try:
            self.data = {
                "Lx": _input_to_meters(self.input_Lx.Text),
                "Ly": _input_to_meters(self.input_Ly.Text),
                "e": _input_to_meters(self.input_extension.Text),
                "dx": _input_to_meters(self.input_dx.Text),
                "dy": _input_to_meters(self.input_dy.Text),
                "Sx": [self.select_symbol_x.SelectedItem],
                "Sy": [self.select_symbol_y.SelectedItem]
            }
        except ValueError:
            TaskDialog.Show('error', 'Invalid input detected')
                

    def OK_Clicked(self, sender, e):
        self._collect_input_data()
        # self.data
        if not self.data:
            return
        self.Close()

    def Cancel_Clicked(self, sender, e):
        self.Close()
def _input_to_meters(value):
    """Convert a textual value to meters."""
    return UnitUtils.ConvertToInternalUnits(float(value), UnitTypeId.Meters)



UI = Grids()
OUT = UI

Thanks.

hi, with 2 radio buttons and an inverted if condition between them can be
it’s a track

Sincerely
christian.stan

2 Likes

@christian.stan

I dont want to use radio button…As shown in the image above, I want to retain the content describing numeric and alphabetic symbols as they are in the dropdown, while using the actual symbol values in the code-behind

Thanks.

Hi,

why not limit yourself to a single combox (X axis “Lx”) and automatically determine by deduction the value for the Y axis (“Ly”)

In addition you can show in an TextBox the value for “Ly”

otherwise to answer the question strictly I would consider working on the MVVM pattern rather than events

2 Likes

here an example with MVVM

code (IronPython3)

import clr
import sys
import System
from System.Collections.Generic import List, Dictionary
from System.Collections.ObjectModel import ObservableCollection
clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)

clr.AddReference("System.Xml")
clr.AddReference("PresentationFramework")
clr.AddReference("System.Xml")
clr.AddReference("PresentationCore")
clr.AddReference("System.Windows")
import System.Windows.Controls 
from System.Windows.Controls import *
import System.Windows.Controls.Primitives 
from System.Collections.Generic import List
from System.IO import StringReader
from System.Xml import XmlReader
from System.Windows import LogicalTreeHelper 
from System.Windows.Markup import XamlReader, XamlWriter
from System.Windows import Window, Application
from System.ComponentModel import INotifyPropertyChanged, PropertyChangedEventArgs

clr.AddReference("IronPython.Wpf")
import wpf

class ViewModel(INotifyPropertyChanged):
    def __init__(self, lstItems):
        super().__init__()
        self._lstItems = ObservableCollection[System.String](lstItems)
        try:
            self.PropertyChanged = None
        except:
            pass
        self._txtValue = ""
        self._itemA = self._lstItems[0] if lstItems else None
        self._itemB = self._lstItems[1] if len(lstItems) > 1 else None
        self._property_changed_handlers = []
        
    # define getter and setter
    
    @property
    def Items(self):
        return self._lstItems
    @Items.setter
    def Items(self, value):
        self._lstItems = value
        self.OnPropertyChanged("Items")
    
    @property
    def TxtValue(self):
        return self._txtValue
    @TxtValue.setter
    def TxtValue(self, value):
        self._txtValue = value
        self.OnPropertyChanged("TxtValue")
        
    @property
    def ItemA(self):
        return self._itemA
    @ItemA.setter
    def ItemA(self, value):
        if value == self._itemB: 
            self.ItemB = None
        self._itemA = value
        self.OnPropertyChanged("ItemA")
        
    @property
    def ItemB(self):
        return self._itemB
    @ItemB.setter
    def ItemB(self, value):
        if value == self._itemA :
            self.ItemA = None
        self._itemB = value
        self.OnPropertyChanged("ItemB")
    
    def OnPropertyChanged(self, property_name):
        event_args = PropertyChangedEventArgs(property_name)
        for handler in self._property_changed_handlers:
            handler(self, event_args)

    # Implementation of add/remove_PropertyChanged
    def add_PropertyChanged(self, handler):
        #print(handler)
        if handler not in self._property_changed_handlers:
            self._property_changed_handlers.append(handler)

    def remove_PropertyChanged(self, handler):
        #print(handler)
        if handler in self._property_changed_handlers:
            self._property_changed_handlers.remove(handler)
                       

class MainForm(Window):
    string_xaml = '''
        <Window 
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                 Title="IronPython WPF Form" Height="200" Width="300">
                <Grid Margin="10">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
        
                    <!-- TextBox -->
                    <Label Grid.Row="0" Grid.Column="0" Content="Enter Text:" VerticalAlignment="Center"/>
                    <TextBox Grid.Row="0" Grid.Column="1" 
                        Name="InputTextBox" Margin="5" VerticalAlignment="Center"
                        Text="{Binding TxtValue, UpdateSourceTrigger=PropertyChanged}" />
        
                    <!-- ComboBox 1 -->
                    <Label Grid.Row="1" Grid.Column="0" Content="Select Option 1:" VerticalAlignment="Center"/>
                    <ComboBox Grid.Row="1" Grid.Column="1" 
                        Name="ComboBox1" Margin="5" VerticalAlignment="Center"
                        ItemsSource="{Binding Items}"
                        SelectedItem="{Binding ItemA, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        
                    <!-- ComboBox 2 -->
                    <Label Grid.Row="2" Grid.Column="0" Content="Select Option 2:" VerticalAlignment="Center"/>
                    <ComboBox Grid.Row="2" Grid.Column="1" 
                        Name="ComboBox2" Margin="5" VerticalAlignment="Center"
                        ItemsSource="{Binding Items}"
                        SelectedItem="{Binding ItemB, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        
                    <!-- Submit Button -->
                    <Button Grid.Row="3" Grid.ColumnSpan="2" Content="Submit" 
                            Name="SubmitButton" Margin="5" Width="80" Height="30" 
                            VerticalAlignment="Bottom" HorizontalAlignment="Center" 
                            Click="ButtonClick"/>
                </Grid>
        </Window>'''
  
    def __init__(self, lstvalue):
        super().__init__()
        self.lstvalue = lstvalue
        wpf.LoadComponent(self, StringReader(MainForm.string_xaml))
        # set DataContext
        self.vm = ViewModel(lstvalue)
        self.DataContext = self.vm
        
    def ButtonClick(self, sender, e):
        try:
            self.Close()
        except Exception as ex:
            print(traceback.format_exc())
            
    
lstValues = IN[0]
out = []

my_window = MainForm(lstValues)
my_window.ShowDialog()
out.append(my_window.vm.TxtValue)
out.append(my_window.vm.ItemA)
out.append(my_window.vm.ItemB)

OUT = out
5 Likes

@c.poupin

First of all, sorry for the late feedback.
I was struggling to resolve another issue related to this one.

Your approach of using INotifyPropertyChanged is challenging for me at the moment, as I haven’t reached an advanced level in WPF and MVVM. However, I managed to solve this issue in my own way while taking your suggestions below into account.

I was unable to implement exception handling from the UI using MVVM in Dynamo, so I used pyrevit instead, and my code works perfectly.

Grids by pyrevit
# -*- coding: UTF-8 -*-
from pyrevit import forms, revit
import wpf, clr

clr.AddReference("PresentationFramework")
clr.AddReference("System.Xml")
clr.AddReference("PresentationCore")
clr.AddReference("System.Windows")

from System.IO import StringReader
from System.Windows.Markup import XamlReader
from System.Windows import Window
from Autodesk.Revit.DB import *
from string import ascii_uppercase


uidoc = __revit__.ActiveUIDocument
doc = __revit__.ActiveUIDocument.Document
app = __revit__.Application


class Grids(Window):
    LAYOUT = '''
        <Window 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Grids"
            Height="Auto"
            Width="300"
            SizeToContent ="Height" 
            ResizeMode="NoResize"
            WindowStartupLocation="CenterScreen">
            <StackPanel Margin="10">
                <!--Input LX-->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Longueur Total suivant X (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   Lx =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_Lx" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Input LY-->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Longueur Total suivant Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   Ly =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_Ly" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Grid Extension -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Extension des Axes X, Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   e =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_extension" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Spacing along X axis -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Espacement suivant X (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   dx =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_dx" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Spacing along Y axis -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Espacement suivant Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   dy =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_dy" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--bubbles symbol -->
                <StackPanel>
                    <TextBlock Text="Choix de symbols pour Lx, Ly:" FontWeight="Bold" Margin="0,0,0,10" />
                    
                    <!-- Symbol Type for Y Axis -->
                    <DockPanel Margin="0,0,0,10" >
                        <TextBlock Text="Type de symbole Sx:" />
                        <ComboBox x:Name="Symbol_x" HorizontalAlignment="Right" SelectionChanged="Symbol_x_SelectionChanged" Width="110">
                            <ComboBoxItem Content="1 2 3 ..." />
                            <ComboBoxItem Content="A B C ..." />
                        </ComboBox>
                    </DockPanel>
                    
                    <!-- Symbol Type for Y Axis -->
                    <DockPanel Margin="0,0,0,10">
                        <TextBlock Text="Type de symbole Sy:" />
                        <TextBox x:Name="Symbol_y" Width="110" HorizontalAlignment="Right"/>
                    </DockPanel>
                </StackPanel>
                
                <!--Separator, OK and Cancel Button-->
                <Separator Margin="0,0,0,10"/>
                <DockPanel HorizontalAlignment="Center">
                    <Button Content="OK" Width="75" Height="25" Margin="0,0,10,0" Click="OK_Clicked" />
                    <Button Content="Cancel" Width="75" Height="25" Click="Cancel_Clicked" />
                </DockPanel>            
            </StackPanel>
        </Window>'''

    def __init__(self):
        wpf.LoadComponent(self, StringReader(Grids.LAYOUT))
        self.data = {}
        self.ShowDialog()

    def Symbol_x_SelectionChanged(self, sender, e):
        selected_item = self.Symbol_x.SelectedItem
        if selected_item.Content == "1 2 3 ...":
            self.Symbol_y.Text = self.Symbol_x.Items[1].Content.ToString()
        else:
            self.Symbol_y.Text = self.Symbol_x.Items[0].Content.ToString()

    def _collect_input_data(self):
        """Collect and convert input data from the UI"""
        # Identify which key has missing or invalid input and alert the user
        if not self.input_Lx.Text:
            forms.alert("Please enter a valid value for : Lx")
        elif not self.input_Ly.Text:
            forms.alert("Please enter a valid value for : Ly")
        elif not self.input_extension.Text:
            forms.alert("Please enter a valid value for : e")
        elif not self.input_dx.Text:
            forms.alert("Please enter a valid value for : dx")
        elif not self.input_dy.Text:
            forms.alert("Please enter a valid value for : dy")
        else:
            try:
                self.data = {
                    "Lx": _input_to_meters(self.input_Lx.Text),
                    "Ly": _input_to_meters(self.input_Ly.Text),
                    "e": _input_to_meters(self.input_extension.Text),
                    "dx": _input_to_meters(self.input_dx.Text),
                    "dy": _input_to_meters(self.input_dy.Text),
                }
            except (ValueError, AttributeError):
                forms.alert("Invalid input or missing selections. Please correct and try again.")

    def symbols_alpha(self, length_x, length_y):
        """Generate Sx and Sy symbols based on lengths of grids"""
        Sx = []
        Sy = []
        if self.Symbol_x.SelectedItem == self.Symbol_x.Items[0]:
            for i in range(length_y):
                if i < len(ascii_uppercase):
                    Sy.append(ascii_uppercase[i])
            Sx = []
        else:
            for i in range(length_x):
                if i < len(ascii_uppercase):
                    Sx.append(ascii_uppercase[i])
            Sy = []
        return Sx, Sy

    def OK_Clicked(self, sender, e):
        self._collect_input_data()
        if not self.data:
            return
        n1 = int(self.data["Lx"] // self.data["dx"])
        if not isclose(self.data["Lx"], n1 * self.data["dx"]):
            length_x = n1 + 2
        else:
            length_x = n1 + 1
        print(length_x)
        n2 = int(self.data["Ly"] // self.data["dy"])
        if not isclose(self.data["Ly"], n2 * self.data["dy"]):
            length_y = n2 + 2
        else:
            length_y = n2 + 1

        # Populate Sx and Sy dynamically based on grid lengths
        self.data["Sx"], self.data["Sy"] = self.symbols_alpha(length_x, length_y)
        print(self.data["Sx"])
        print(self.data["Sy"])
        self.Close()

    def Cancel_Clicked(self, sender, e):
        self.data = {}
        self.Close()


def _input_to_meters(value):
    """Convert a textual value to meters."""
    return UnitUtils.ConvertToInternalUnits(float(value), UnitTypeId.Meters)

def isclose(a, b, rel_tol=1e-9, abs_tol=0.0):
    return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)


def create_lines(lx, ly, d, e, y_axis=False):
    """Create lines along X, Y axis """
    if y_axis:
        lx, ly = ly, lx
    n = lx // d
    points_X = []
    i = 0
    while i <= int(n):
        points_X.append(d * i - lx / 2)
        i += 1
    if not isclose(lx, n * d):
        points_X.append(lx / 2)
    points_Y = [-ly / 2 - e, ly / 2 + e]

    if y_axis:
        return [Line.CreateBound(*(XYZ(x, y, 0) for x in points_Y)) for y in points_X]

    return [Line.CreateBound(*(XYZ(x, y, 0) for y in points_Y)) for x in points_X]



def create_grids(lines_x, lines_y, Sx, Sy, doc):
    with Transaction(doc, "Create Grids") as t:
        t.Start()
        grids_X = [Grid.Create(doc, line) for line in lines_x]
        print(len(grids_X))
        grids_Y = [Grid.Create(doc, line) for line in lines_y]
        print(len(grids_Y))
        if Sx:  # If Sx is populated, assign names to grids_X
            print(len(Sx))
            for grid, symbol in zip(grids_X, Sx):
                grid.Name = symbol
            for idx, grid in enumerate(grids_Y):
                grid.Name = str(idx+1)
        if Sy:  # If Sy is populated, assign names to grids_Y
            print(len(Sy))
            for grid, symbol in zip(grids_Y, Sy):
                grid.Name = symbol
            for idx, grid in enumerate(grids_X):
                grid.Name = str(idx+1)
        t.Commit()


def main(doc=None):
    doc = doc or revit.doc
    form = Grids()
    if not form.data:
        return
    data = form.data
    lines_x = create_lines(data["Lx"], data["Ly"], data["dx"], data["e"])
    lines_y = create_lines(data["Lx"], data["Ly"], data["dy"], data["e"], y_axis=True)
    create_grids(lines_x, lines_y, data["Sx"], data["Sy"], doc)


if __name__ == "__main__":
    main()

Thanks.

1 Like

@c.poupin

Finally, I was able to handle exceptions from the UI in Dynamo using MessageBox from System.Windows.Forms, which allows me to continue the code logic and create grids. (For some reason, the code does not work 100% as expected when switching between alphabetic and numeric symbols, whereas it works fine in PyRevit.)

Dynamo Grids
import clr    
import sys
import System

clr.AddReference("IronPython.Wpf")
clr.AddReference("PresentationFramework")
clr.AddReference("System.Xml")
clr.AddReference("PresentationCore")
clr.AddReference("System.Windows")
clr.AddReference('System.Windows.Forms')
clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)
from System.IO import StringReader
from System.Windows.Markup import XamlReader, XamlWriter
from System.Windows import Window, Application
import System.Windows.Controls 
from System.Windows.Controls import *
from System.Windows.Forms import MessageBox
from string import ascii_uppercase
import wpf
# Import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB

clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

doc = DocumentManager.Instance.CurrentDBDocument


class Grids(Window):
    LAYOUT = '''
        <Window 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Grids"
            Height="Auto"
            Width="300"
            SizeToContent ="Height" 
            ResizeMode="NoResize"
            WindowStartupLocation="CenterScreen">
            <StackPanel Margin="10">
                <!--Input LX-->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Longueur Total suivant X (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   Lx =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_Lx" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Input LY-->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Longueur Total suivant Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   Ly =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_Ly" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Grid Extension -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Extension des Axes X, Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   e =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_extension" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Spacing along X axis -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Espacement suivant X (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   dx =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_dx" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Spacing along Y axis -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Espacement suivant Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   dy =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_dy" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--bubbles symbol -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text="Choix de symbols pour Lx, Ly:" FontWeight="Bold" Margin="0,0,0,10" />
                    
                    <!-- Symbol Type for X Axis -->
                    <DockPanel Margin="0,0,0,10" >
                        <TextBlock Text="Type de symbole Sx:" />
                        <ComboBox x:Name="Symbol_x" HorizontalAlignment="Right" SelectionChanged="Symbol_x_SelectionChanged" Width="110">
                            <ComboBoxItem Content="1 2 3 ..." />
                            <ComboBoxItem Content="A B C ..." />
                        </ComboBox>
                    </DockPanel>
                    
                    <!-- Symbol Type for Y Axis -->
                    <DockPanel Margin="0,0,0,10">
                        <TextBlock Text="Type de symbole Sy:" />
                        <TextBox x:Name="Symbol_y" Width="110" Height="25" HorizontalAlignment="Right"/>
                    </DockPanel>
                </StackPanel>
                
                <!--Separator, OK and Cancel Button-->
                <Separator Margin="0,0,0,10"/>
                <DockPanel HorizontalAlignment="Center">
                    <Button Content="OK" Width="75" Height="25" Margin="0,0,10,0" Click="OK_Clicked" />
                    <Button Content="Cancel" Width="75" Height="25" Click="Cancel_Clicked" />
                </DockPanel>            
            </StackPanel>
        </Window>'''

    def __init__(self):
        wpf.LoadComponent(self, StringReader(Grids.LAYOUT))
        self.data = {}
        self.ShowDialog()

    def Symbol_x_SelectionChanged(self, sender, e):
        selected_item = self.Symbol_x.SelectedItem
        if selected_item.Content == "1 2 3 ...":
            self.Symbol_y.Text = self.Symbol_x.Items[1].Content.ToString()
        else:
            self.Symbol_y.Text = self.Symbol_x.Items[0].Content.ToString()

    def collect_input_data(self):
        """Collect and convert input data from the UI"""
        # Identify which key has missing or invalid input and alert the user
        try:
            if not self.input_Lx.Text:
                MessageBox.Show("Please enter a valid value for: Lx")
                return
            Lx = input_to_meters(self.input_Lx.Text)

            if not self.input_Ly.Text:
                MessageBox.Show("Please enter a valid value for: Ly")
                return
            Ly = input_to_meters(self.input_Ly.Text)
            
            if not self.input_extension.Text:
                MessageBox.Show("Please enter a valid value for: e")
                return
            e = input_to_meters(self.input_extension.Text)

            if not self.input_dx.Text:
                MessageBox.Show("Please enter a valid value for: dx")
                return
            dx = input_to_meters(self.input_dx.Text)

            if not self.input_dy.Text:
                MessageBox.Show("Please enter a valid value for: dy")
                return
            dy = input_to_meters(self.input_dy.Text)
            
            self.data = {"Lx": Lx, "Ly": Ly, "e": e, "dx": dx, "dy": dy}

        except ValueError:
            MessageBox.Show("Invalid input detected. Please enter numeric values only.")
            self.data = {}  # Clear the data dictionary if any input is invalid
            
    def symbols_alpha(self, length_x, length_y):
        """Generate Sx and Sy symbols based on lengths of grids"""
        Sx = []
        Sy = []
        if self.Symbol_x.SelectedItem == self.Symbol_x.Items[0]:
            for i in range(length_y):
                if i < len(ascii_uppercase):
                    Sy.append(ascii_uppercase[i])
            Sx = []
        else:
            for i in range(length_x):
                if i < len(ascii_uppercase):
                    Sx.append(ascii_uppercase[i])
            Sy = []
        return Sx, Sy
    
    def OK_Clicked(self, sender, e):
        self.collect_input_data()
        if not self.data:
            return
        n1 = int(self.data["Lx"] // self.data["dx"])
        if not isclose(self.data["Lx"], n1 * self.data["dx"]):
            length_x = n1 + 2
        else:
            length_x = n1 + 1
        n2 = int(self.data["Ly"] // self.data["dy"])
        if not isclose(self.data["Ly"], n2 * self.data["dy"]):
            length_y = n2 + 2
        else:
            length_y = n2 + 1

        # Populate Sx and Sy dynamically based on grid lengths
        self.data["Sx"], self.data["Sy"] = self.symbols_alpha(length_x, length_y)
        self.Close()
        
    def Cancel_Clicked(self, sender, e):
        self.Close()
        
def input_to_meters(value):
    """Convert a textual value to meters."""
    return UnitUtils.ConvertToInternalUnits(float(value), UnitTypeId.Meters)
    
def isclose(a, b, rel_tol=1e-9, abs_tol=0.0):
    return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
    
def create_lines(lx, ly, d, e, y_axis=False):
    """Create lines along X, Y axis """
    if y_axis:
        lx, ly = ly, lx
    n = lx // d
    points_X = []
    i = 0
    while i <= int(n):
        points_X.append(d * i - lx / 2)
        i += 1
    if not isclose(lx, n * d):
        points_X.append(lx / 2)
    points_Y = [-ly / 2 - e, ly / 2 + e]

    if y_axis:
        return [Line.CreateBound(*(XYZ(x, y, 0) for x in points_Y)) for y in points_X]

    return [Line.CreateBound(*(XYZ(x, y, 0) for y in points_Y)) for x in points_X]
    
def create_grids(lines_x, lines_y, Sx, Sy, doc):
    with Transaction(doc, "Create Grids") as t:
        t.Start()
        grids_X = [Grid.Create(doc, line) for line in lines_x]
        print(len(grids_X))
        grids_Y = [Grid.Create(doc, line) for line in lines_y]
        print(len(grids_Y))
        if Sx:  # If Sx is populated, assign names to grids_X
            print(len(Sx))
            for grid, symbol in zip(grids_X, Sx):
                grid.Name = symbol
            for idx, grid in enumerate(grids_Y):
                grid.Name = str(idx+1)
        if Sy:  # If Sy is populated, assign names to grids_Y
            print(len(Sy))
            for grid, symbol in zip(grids_Y, Sy):
                grid.Name = symbol
            for idx, grid in enumerate(grids_X):
                grid.Name = str(idx+1)
        t.Commit()
    
UI = Grids()
data = UI.data
if data:
    length_x = int(data["Lx"] // data["dx"])
    length_y = int(data["Ly"] // data["dy"])
    Sx, Sy = UI.symbols_alpha(length_x, length_y)

    lines_x = create_lines(data["Lx"], data["Ly"], data["dx"], data["e"])
    lines_y = create_lines(data["Lx"], data["Ly"], data["dy"], data["e"], y_axis=True)

    Grids = create_grids(lines_x, lines_y, Sx, Sy, doc)

OUT = [l.ToProtoType() for l in lines_x], [l.ToProtoType() for l in lines_y]

Thanks.