How to handle input data from a WPF window?

Hi All, @ErikFrits @Luffy11
I developed my first application using WPF and pyrevit, which creates a rectangular tank. However, since I am still learning WPF and haven’t mastered it yet, I need some guidance on how to manage my data and structure my code properly to use WPF functions effectively, particularly event handlers."
here the application UI
Tank

In my code, I should first check/create a concrete material with a specific compressive strength that is used for the structural elements. I’m unsure about how and where to introduce my input parameters in relation to the click eventOK.’

In this case, should I start the transaction for creating the material inside the ‘OK’ function using the input value of ‘concrete compressive,’ or is there a way to create the material using this input value outside the ‘OK’ function and then use the created material inside it?

pls check my script and xaml file below

script
# -*- coding: UTF-8 -*-
from pyrevit import forms
import wpf, os, clr
clr.AddReference("System")
from System.Windows import Window
import Autodesk
from Autodesk.Revit.DB import *
from Autodesk.Revit.UI import *
from pyrevit.forms import WPFWindow

doc = __revit__.ActiveUIDocument.Document
uidoc = __revit__.ActiveUIDocument
# get the path for the script
PATH_SCRIPT = os.path.dirname(__file__)

#get the default wall type within the document
wall_Type_Id = doc.GetDefaultElementTypeId(ElementTypeGroup.WallType)
def_wall_Type = doc.GetElement(wall_Type_Id)

#get the default floor type within the document
floor_Type_Id = doc.GetDefaultElementTypeId(ElementTypeGroup.FloorType)
def_floor_Type = doc.GetElement(floor_Type_Id)

#collecting levels
levels = FilteredElementCollector(doc).OfCategory(
    BuiltInCategory.OST_Levels).WhereElementIsNotElementType().ToElements()
#collecting materials
materials = FilteredElementCollector(doc).WherePasses(ElementCategoryFilter(BuiltInCategory.OST_Materials))



# get the base level 0.00
level = [l for l in levels if
         l.LookupParameter('Elévation').AsValueString() == '0.00' or l.LookupParameter('Elévation').AsDouble() == 0]

if len(level) > 0:
    lvl = level[0]
    lvl_Id = lvl.Id


# the form class
class ModalForm(Window):
    def __init__(self):
        path_xaml_file = os.path.join(PATH_SCRIPT, 'cofrage_modifié.xaml')
        wpf.LoadComponent(self, path_xaml_file)
        self.ShowDialog()
    def ok_button(self, sender, e):
        A = self.A_value.Text
        A = UnitUtils.ConvertToInternalUnits(float(A), UnitTypeId.Meters)
        B = self.B_value.Text
        B = UnitUtils.ConvertToInternalUnits(float(B), UnitTypeId.Meters)
        H = self.H_value.Text
        H = UnitUtils.ConvertToInternalUnits(float(H), UnitTypeId.Meters)
        ep = self.ep_value.Text
        ep = UnitUtils.ConvertToInternalUnits(float(ep), UnitTypeId.Meters)
        contrainte = self.contrainte_value.Text
        # get/create the concrete material with the specific parameters
        material = [i for i in materials if i.Name == "Béton - Coulé sur place - Béton" + str(contrainte)]

        if len(material) > 0:
            mat = material[0]
            m_Id = mat.Id
        else:
            with Transaction(doc, 'Get/create concrete material') as t:
                t.Start()
                m_Id = Material.Create(doc, "Béton - Coulé sur place - Béton" + str(contrainte))
                mat = doc.GetElement(m_Id)
                mat.MaterialClass = "Béton"
                mat.MaterialCategory = "Béton"
                mat.Color = Color(192, 192, 192)
                mat.Shininess = 128
                mat.Smoothness = 50
                asset = StructuralAsset("ConcreteStructuralAsset", StructuralAssetClass.Concrete)
                asset.ConcreteCompression = UnitUtils.ConvertToInternalUnits(float(contrainte), UnitTypeId.Megapascals)
                asset.Density = UnitUtils.ConvertToInternalUnits(2500, UnitTypeId.KilogramsPerCubicMeter)
                asset.SetPoissonRatio(0.2)
                asset.SetYoungModulus(UnitUtils.ConvertToInternalUnits(11000 * float(contrainte) ** (1 / 3), UnitTypeId.Megapascals))
                struc = PropertySetElement.Create(doc, asset)
                mat.SetMaterialAspectByPropertySet(MaterialAspect.Structural, struc.Id)
                appar_Id = ElementId(177984)
                mat.AppearanceAssetId = appar_Id
                t.Commit()
        # Tank geometry 
        Pt0 = XYZ(-(A + ep)/ 2 , -(B + ep)/ 2 , 0)

        c0 = XYZ(-(A / 2 + ep), -(B / 2 + ep) , 0)

        Pt1 = XYZ((A + ep)/ 2 , -(B + ep)/ 2 , 0)

        c1 = XYZ(A / 2 + ep , -(B / 2 + ep), 0)

        Pt2 = XYZ((A + ep)/ 2 , (B + ep)/ 2 , 0)

        c2 = XYZ(A / 2 + ep , B / 2 + ep , 0)

        Pt3 = XYZ(-(A + ep)/ 2 , (B + ep)/ 2, 0)

        c3 = XYZ(-(A / 2 + ep) , B / 2 + ep , 0)

        Lines = []

        crvs = []
        Line1 = Line.CreateBound(Pt0, Pt1)
        Lines.append(Line1)

        crv1 = Line.CreateBound(c0, c1)
        crvs.append(crv1)

        Line2 = Line.CreateBound(Pt1, Pt2)
        Lines.append(Line2)

        crv2 = Line.CreateBound(c1, c2)
        crvs.append(crv2)

        Line3 = Line.CreateBound(Pt2, Pt3)
        Lines.append(Line3)

        crv3 = Line.CreateBound(c2, c3)
        crvs.append(crv3)

        Line4 = Line.CreateBound(Pt3, Pt0)
        Lines.append(Line4)

        crv4 = Line.CreateBound(c3, c0)
        crvs.append(crv4)

        sloop = Line.CreateBound(XYZ(0, 0, 0), XYZ(1/0.3048, 0, 0))

        loop = CurveLoop()

        for crv in crvs:
            loop.Append(crv)

        curves = [loop]

        # Create Plane and Sketch_plan:
        normal = XYZ.BasisZ

        origin = XYZ.Zero
        geomPlane = Plane.CreateByNormalAndOrigin(normal, origin)
        with Transaction(doc, 'create Manhole') as t:
            t.Start()
            # new wall type
            voile = def_wall_Type.Duplicate('voile-BA_20')
            # new floor type
            Dalle = def_floor_Type.Duplicate('Dalle_BA_20')
            cs1 = voile.GetCompoundStructure()
            cs2 = Dalle.GetCompoundStructure()

            for structLayer1, structLayer2 in zip(cs1.GetLayers(), cs2.GetLayers()):
                cs1.SetLayerFunction(structLayer1.LayerId, MaterialFunctionAssignment.Structure)
                cs1.SetMaterialId(structLayer1.LayerId, m_Id)
                cs1.SetLayerWidth(0, ep)
                cs2.SetLayerFunction(structLayer2.LayerId, MaterialFunctionAssignment.Structure)
                cs2.SetMaterialId(structLayer2.LayerId, m_Id)
                cs2.SetLayerWidth(0, ep)

            voile.SetCompoundStructure(cs1)
            Dalle.SetCompoundStructure(cs2)
            for l in Lines:
                # create walls
                wall = Wall.Create(doc, l, voile.Id, lvl_Id, H, 0.00, False, True)
                # create floor
                bot_floor = Floor.Create(doc, curves, Dalle.Id, lvl_Id, True, sloop, 0)
            t.Commit()
            self.Close()

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

form = ModalForm()```
xaml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	Title="Window1"
	Height="350"
	Width="330">
	<Grid>
		<Label
			Content="Largeur A (m)"
			Height="25"
			Width="90"
			VerticalAlignment="Top"
			HorizontalAlignment="Left"
			Grid.Row="0"
			Grid.Column="0"
			Margin="25,25,0,0" />
		<TextBox
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Top"
			Width="100"
			Height="25"
			x:Name="A_value"
			Margin="0,25,25,0" />
		<Label
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Top"
			Content="Largeur B (m)"
			Width="90"
			Height="25"
			Margin="25,75,0,0" />
		<TextBox
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Top"
			Height="25"
			Width="100"
			x:Name="B_value"
			Margin="0,75,25,0" />
		<Label
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Top"
			Width="85"
			Height="25"
			Content="Hauteur H (m)"
			Margin="25,125,0,0" />
		<TextBox
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Top"
			x:Name="H_value"
			Width="100"
			Height="25"
			Margin="0,125,25,0" />
		<Label
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Bottom"
			Height="25"
			Content="epaisseur Mur (m)"
			Width="105"
			Margin="25,0,0,120" />
		<TextBox
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Bottom"
			x:Name="ep_value"
			Height="25"
			Width="100"
			Margin="0,0,25,120" />
		<Label
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Bottom"
			Height="25"
			Content="Contrainte de béton (MPA)"
			Width="155"
			Margin="25,0,0,75" />
		<TextBox
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			Height="25"
			Width="100"
			VerticalAlignment="Bottom"
			Margin="0,217,25,75"
			x:Name="contrainte_value" />
		<Button
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Bottom"
			Width="90"
			Height="25"
			Margin="25,0,0,20"
			Content="OK"
			x:Name="OK_button"
			Click="ok_button" />
		<Button
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Bottom"
			Height="25"
			Width="90"
			Margin="0,0,25,20"
			Content="Cancel"
			x:Name="Cancel_button"
			Click="cancel_button" />
	</Grid>
</Window>

Thanks.

Hi @REDO_79
there are various patterns to handle data in a WPF application, but usually the common practice is to separate the UI from the business logic.

In your case, you can add the A, B, H and so on variables as attibutes of the ModalForm class, or even better create a separate class (or it could be just a dictionary to keep it simple) to hold the data and use a single attribute like “self.data” in the ModalForm.
In the __init__ method, initialize self.data = None.
In the ok_button method you just save the data into self.data and call self.Close();
then in the main script you can check and access self.data and run the business logic

1 Like

@sanzoghenzo

Can you please show me an example of how to create a separate class for data, and then call it in the ModalForm?

Thanks

Have a look at this version of your script.
Chatgpt is not good at generating stuff from scratch but decent when it is about fixing code. Too little time for me to take the time to understand the whole thing.

you should try erik fritz course about wpf to get you started properly


# -*- coding: UTF-8 -*-
from pyrevit import forms
import wpf, os, clr
clr.AddReference("System")
from System.Windows import Window
import Autodesk
from Autodesk.Revit.DB import *
from Autodesk.Revit.UI import *
from pyrevit.forms import WPFWindow

doc = __revit__.ActiveUIDocument.Document
uidoc = __revit__.ActiveUIDocument
# get the path for the script
PATH_SCRIPT = os.path.dirname(__file__)

#get the default wall type within the document
wall_Type_Id = doc.GetDefaultElementTypeId(ElementTypeGroup.WallType)
def_wall_Type = doc.GetElement(wall_Type_Id)

#get the default floor type within the document
floor_Type_Id = doc.GetDefaultElementTypeId(ElementTypeGroup.FloorType)
def_floor_Type = doc.GetElement(floor_Type_Id)

#collecting levels
levels = FilteredElementCollector(doc).OfCategory(
    BuiltInCategory.OST_Levels).WhereElementIsNotElementType().ToElements()
#collecting materials
materials = FilteredElementCollector(doc).WherePasses(ElementCategoryFilter(BuiltInCategory.OST_Materials))

# get the base level 0.00
level = [l for l in levels if
         l.LookupParameter('Elévation').AsValueString() == '0.00' or l.LookupParameter('Elévation').AsDouble() == 0]

if len(level) > 0:
    lvl = level[0]
    lvl_Id = lvl.Id

# the form class
class ModalForm(Window):
    def __init__(self):
        path_xaml_file = os.path.join(PATH_SCRIPT, 'cofrage_modifié.xaml')
        wpf.LoadComponent(self, path_xaml_file)
        self.ShowDialog()

    def collect_input_data(self):
        """Collect and convert input data from the UI"""
        try:
            A = UnitUtils.ConvertToInternalUnits(float(self.A_value.Text), UnitTypeId.Meters)
            B = UnitUtils.ConvertToInternalUnits(float(self.B_value.Text), UnitTypeId.Meters)
            H = UnitUtils.ConvertToInternalUnits(float(self.H_value.Text), UnitTypeId.Meters)
            ep = UnitUtils.ConvertToInternalUnits(float(self.ep_value.Text), UnitTypeId.Meters)
            contrainte = float(self.contrainte_value.Text)
            return A, B, H, ep, contrainte
        except ValueError:
            forms.alert("Invalid input. Please enter numeric values.")
            return None

    def get_or_create_concrete_material(self, contrainte):
        """Retrieve or create the required concrete material"""
        material = [i for i in materials if i.Name == "Béton - Coulé sur place - Béton{}".format(contrainte)]
        if len(material) > 0:
            return material[0].Id
        else:
            with Transaction(doc, 'Get/create concrete material') as t:
                t.Start()
                m_Id = Material.Create(doc, "Béton - Coulé sur place - Béton{}".format(contrainte))
                mat = doc.GetElement(m_Id)
                mat.MaterialClass = "Béton"
                mat.MaterialCategory = "Béton"
                mat.Color = Color(192, 192, 192)
                mat.Shininess = 128
                mat.Smoothness = 50
                asset = StructuralAsset("ConcreteStructuralAsset", StructuralAssetClass.Concrete)
                asset.ConcreteCompression = UnitUtils.ConvertToInternalUnits(float(contrainte), UnitTypeId.Megapascals)
                asset.Density = UnitUtils.ConvertToInternalUnits(2500, UnitTypeId.KilogramsPerCubicMeter)
                asset.SetPoissonRatio(0.2)
                asset.SetYoungModulus(UnitUtils.ConvertToInternalUnits(11000 * float(contrainte) ** (1 / 3), UnitTypeId.Megapascals))
                struc = PropertySetElement.Create(doc, asset)
                mat.SetMaterialAspectByPropertySet(MaterialAspect.Structural, struc.Id)
                appar_Id = ElementId(177984)
                mat.AppearanceAssetId = appar_Id
                t.Commit()
            return m_Id

    def ok_button(self, sender, e):
        """OK button handler"""
        input_data = self.collect_input_data()
        if not input_data:
            return
        A, B, H, ep, contrainte = input_data

        # Get or create concrete material
        m_Id = self.get_or_create_concrete_material(contrainte)

        # Now proceed with the geometry and transaction logic
        # Create tank walls, floor, etc.
        self.create_tank_geometry(A, B, H, ep, m_Id)

        self.Close()

    def create_tank_geometry(self, A, B, H, ep, m_Id):
        """Creates the tank geometry inside a transaction"""
        Pt0 = XYZ(-(A + ep) / 2, -(B + ep) / 2, 0)
        c0 = XYZ(-(A / 2 + ep), -(B / 2 + ep), 0)
        Pt1 = XYZ((A + ep) / 2, -(B + ep) / 2, 0)
        c1 = XYZ(A / 2 + ep, -(B / 2 + ep), 0)
        Pt2 = XYZ((A + ep) / 2, (B + ep) / 2, 0)
        c2 = XYZ(A / 2 + ep, B / 2 + ep, 0)
        Pt3 = XYZ(-(A + ep) / 2, (B + ep) / 2, 0)
        c3 = XYZ(-(A / 2 + ep), B / 2 + ep, 0)

        Lines = [Line.CreateBound(Pt0, Pt1), Line.CreateBound(Pt1, Pt2), Line.CreateBound(Pt2, Pt3), Line.CreateBound(Pt3, Pt0)]
        crvs = [Line.CreateBound(c0, c1), Line.CreateBound(c1, c2), Line.CreateBound(c2, c3), Line.CreateBound(c3, c0)]

        sloop = Line.CreateBound(XYZ(0, 0, 0), XYZ(1 / 0.3048, 0, 0))

        loop = CurveLoop()
        for crv in crvs:
            loop.Append(crv)
        curves = [loop]

        # Create Plane and Sketch_plan:
        with Transaction(doc, 'Create Tank Geometry') as t:
            t.Start()
            # new wall type
            voile = def_wall_Type.Duplicate('voile-BA_20')
            # new floor type
            Dalle = def_floor_Type.Duplicate('Dalle_BA_20')
            cs1 = voile.GetCompoundStructure()
            cs2 = Dalle.GetCompoundStructure()

            for structLayer1, structLayer2 in zip(cs1.GetLayers(), cs2.GetLayers()):
                cs1.SetLayerFunction(structLayer1.LayerId, MaterialFunctionAssignment.Structure)
                cs1.SetMaterialId(structLayer1.LayerId, m_Id)
                cs1.SetLayerWidth(0, ep)
                cs2.SetLayerFunction(structLayer2.LayerId, MaterialFunctionAssignment.Structure)
                cs2.SetMaterialId(structLayer2.LayerId, m_Id)
                cs2.SetLayerWidth(0, ep)

            voile.SetCompoundStructure(cs1)
            Dalle.SetCompoundStructure(cs2)
            for l in Lines:
                # create walls
                Wall.Create(doc, l, voile.Id, lvl_Id, H, 0.00, False, True)
                # create floor
                Floor.Create(doc, curves, Dalle.Id, lvl_Id, True, sloop, 0)
            t.Commit()

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

form = ModalForm()
1 Like

But this doesn’t solve the problem of having the business logic inside the class at all :sweat_smile:

@REDO_79 creating classes in python is just a step above the basic programming tutorials, I suggest you spend some time to learn about classes and OOP (Object Oriented Programming).
Here I define a class that can hold the data you need to pass to the logic

class TankData:
    def __init__(self, a, b, h, ep, contrainte):
        self.a = a
        self.b = b
        self.h = h
        self.ep = ep
        self.contrainte = contrainte

that you can use in the form like that

class TankInputForm(Window):
    def __init__(self):
        self.data = None
        # rest of the init method...

    def ok_button(self, sender, e):
        # calcualte A, B, H, ep and contrainte... then:
        self.data = TankData(A, B, H, ep, contrainte)
        self.close()

in the main script (i prefer to use a main function and just call main() as the last line of my script so I can exit early):

def main():
    tank_form = TankInputForm()
    if not tank_form.data:
        return
    # call the logic that does the transactions and reads the data from tank_form.data

Note that you can do the same without the data class and using a dictionary:
self.data = {"a": A, "b": B, ...}

The key point here is when you Close() a window, the instance is still in memory and you can access any of its attributes/properties to read the data it contains.

1 Like

Thanks, @Jean-Marc and @sanzoghenzo , for your help. I now understand, through your examples, how to handle input data from a WPF window and use it in my code.

Thanks.

3 Likes