29 mars 2023

[Dynamo += Python] WPF avec IronPython et MVVM







Un article assez technique qui ne sera peut-être pas souvent utilisé dans Dynamo, mais je tenais à partager un exemple de MVVM via IronPython avec Dynamo.

#MVVM #IronPython #DynamoBIM

if this article is not in your language, use the Google Translate widget ⬈ (bottom of page for Mobile version )

L'architecture MVVM (Model View ViewModel) est un modèle de conception logicielle qui permet de séparer les différentes responsabilités d'une application en trois couches distinctes : le modèle (Model), la vue (View) et le ViewModel (ViewModel).

  • Le modèle est responsable de la gestion des données de l'application. Il peut s'agir de données stockées dans une base de données, dans des fichiers ou de données générées dynamiquement. Le modèle expose des méthodes pour accéder et modifier ces données.
  • La vue est responsable de l'interface utilisateur de l'application. Elle est responsable de l'affichage des données du modèle et de la gestion des interactions avec l'utilisateur. La vue est généralement mise en œuvre en utilisant des technologies telles que XAML ou HTML.
  • Le ViewModel est responsable de la logique métier de l'application. Il est responsable de la communication entre la vue et le modèle. Le ViewModel contient des propriétés qui sont liées aux éléments de la vue et expose des commandes qui sont déclenchées par les événements de la vue. Le ViewModel est également responsable de la validation des données et de la gestion des erreurs.


L'un des avantages de l'architecture MVVM c'est qu'elle permet aux développeurs de travailler sur des parties distinctes de l'application sans affecter les autres parties.

Cependant, l'architecture MVVM peut être plus complexe que d'autres modèles de conception. Elle peut par ailleurs nécessiter des compétences supplémentaires pour les développeurs qui ne sont pas familiers avec ce modèle.


Un élément clé pour la création d'un MVVM (WPF) est l'interface INotifyPropertyChanged .
Cette interface permet de notifier la vue (View) lorsqu'une propriété dans le ViewModel (ViewModel) a été modifiée, de sorte que la vue puisse être mise à jour en conséquence.

Lorsque le ViewModel modifie une propriété, il déclenche un événement PropertyChanged pour notifier la vue qu'une propriété a été modifiée. La vue écoute cet événement et met à jour son interface utilisateur en réaction.


Maintenant, voyons comment implémenter tout ceci avec IronPython, prenons pour exemple un mapping de familles Revit 

  • Le code xaml représente la Vue

  • La classe DataModel représente le Model (Donnée d'entrée) au travers d'une ObservableCollection[Type]. Ainsi, on modifiera directement cette collection au travers du MVVM 

  • La classe MyDataViewModel constitue la Data (classe qui hérite de la classe ViewModel et de l'interface INotifyPropertyChanged
Ces 3 parties pourraient être dans des fichiers différents (fichier xaml et Python) mais pour l'exemple tout est regroupé dans le meme script Dynamo




....

code 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"
    xmlns:local="clr-namespace:WpfApplication1"
    mc:Ignorable="d" 
    Height="600" 
    Width="600" 
    ResizeMode="NoResize"
    Title="A" 
    WindowStartupLocation="CenterScreen" 
    Topmost="True" 
    SizeToContent="Width">
    <Grid Margin="10,0,10,10">
        <Label x:Name="selection_label" Content="Select Item" HorizontalAlignment="Left" Height="30"
            VerticalAlignment="Top"/>
        <Button x:Name="button_select" 
                Content="Select" 
                HorizontalAlignment="Center" 
                Height="26" Margin="0,63,0,0" 
                VerticalAlignment="Bottom" 
                Width="200" 
                Click="ButtonClick"/>
        <DataGrid   x:Name="dataGrid" 
                    AutoGenerateColumns="False"
                    Margin="10,30,10,30"
                    BorderThickness="1"
                    RowHeaderWidth="0"
                    CanUserSortColumns="True"
                    CanUserResizeColumns = "False"
                    VerticalScrollBarVisibility="Auto"
                    ItemsSource="{Binding}">
            <DataGrid.Columns>
                <DataGridTextColumn Header="FamilyName" 
                                Binding="{Binding FamilyName}" 
                                IsReadOnly="True" 
                                Width="250"/>
                <DataGridTextColumn Header="Name" 
                                Binding="{Binding Name}" 
                                IsReadOnly="True" 
                                Width="100"/>
                <DataGridTemplateColumn Header="Param">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <ComboBox x:Name="Combobox"
                                ItemsSource="{Binding LstValue}" 
                                SelectedItem="{Binding SelectValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
                                Width="200"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

code python

import clr  
import sys
import System
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


sys.path.append(r'C:Program Files (x86)IronPython 2.7Lib')
sys.path.append(r'C:Program Files (x86)IronPython 2.7DLLs')
clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)

try:
    clr.AddReference("IronPython.Wpf")
    clr.AddReference('System.Core')
    clr.AddReference('System.Xml')
    clr.AddReference('PresentationCore')
    clr.AddReference('PresentationFramework')
    clr.AddReferenceByPartialName("WindowsBase")
except IOError:
    raise
    
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

try:
    import wpf
    import time
except ImportError:
    raise

class XamlLoader(Window):
    
    def __init__(self, xaml_str):
        self.ui = wpf.LoadComponent(self, StringReader(xaml_str))
        self.ui.Title = "Select Types"
        
    def ButtonClick(self, sender, e):
        self.DialogResult = True
        self.Close()
    
    def __getattr__(self, item):
        """Maps values to attributes.
        Only called if there *isn't* an attribute with this name
        """
        return self.ui.FindName(item)

class ViewModelBase(INotifyPropertyChanged):
    """
    base view model class that implements the INotifyPropertyChanged interface
    """
    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 MyDataViewModel(ViewModelBase):
    """
    a view model class that inherits from ViewModelBase
    """
    def __init__(self):
        ViewModelBase.__init__(self)
        self._Name = ""
        self._FamilyName = ""
        # define a attribute to store the selected value from conbobox (binding)
        self._SelectValue = ""
        self._LstValue = ObservableCollection[System.String]()
    
    # Define all getters and setters properties 
    @property
    def Name(self):
        return self._Name

    @Name.setter
    def Name(self, value):
        self._Name = value
        # Raise the PropertyChanged event with the name of the changed property
        self.RaisePropertyChanged("Name")
        
    @property
    def FamilyName(self):
        return self._FamilyName

    @FamilyName.setter
    def FamilyName(self, value):
        self._FamilyName = value
        # Raise the PropertyChanged event with the name of the changed property
        self.RaisePropertyChanged("FamilyName")
        
    @property
    def SelectValue(self):
        return self._SelectValue

    @SelectValue.setter
    def SelectValue(self, value):
        self._SelectValue = value
        # Raise the PropertyChanged event with the name of the changed property
        self.RaisePropertyChanged("SelectValue")
        
    @property
    def LstValue(self):
        return self._LstValue
        
    @LstValue.setter
    def LstValue(self, lst_value):
        self._LstValue = ObservableCollection[System.String](lst_value)
        # Raise the PropertyChanged event with the name of the changed property
        self.RaisePropertyChanged("LstValue")
        
class DataModel(XamlLoader):
    """
    class to get/set Data
    """
    def __init__(self, xaml):
        super(DataModel, self).__init__(xaml)
        #
        used_DoorsTypeIds = set(list(FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Doors)
                                                                    .WhereElementIsNotElementType()
                                                                    .Select(lambda x : x.GetTypeId())))
        #
        self.Net_dict_DType = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Doors)
                                                        .WhereElementIsElementType()
                                                        .Select(lambda x : KeyValuePair[System.String, DB.Element](x.FamilyName +":"+ Element.Name.GetValue(x), x))
                                                        .ToDictionary(lambda kvp: kvp.Key, lambda kvp: kvp.Value)   
        #
        # Create an ObservableCollection of MyDataViewModel objects
        self.data = ObservableCollection[MyDataViewModel]()
        for doorTypeId in used_DoorsTypeIds:
            doorType = doc.GetElement(doorTypeId)
            en = MyDataViewModel()
            en.Name = Element.Name.GetValue(doorType)
            en.FamilyName = doorType.FamilyName
            # set list for combobox 
            en.LstValue = self.Net_dict_DType.Keys 
            self.data.Add(en)
        # set wpf grid DataContext 
        self.ui.DataContext = self.data

    
xaml = IN[0]
objData = DataModel(xaml)
objData.ui.ShowDialog()
dict_DType = objData.Net_dict_DType

OUT = [[x.SelectValue, dict_DType[x.SelectValue] if  dict_DType.ContainsKey(x.SelectValue) else None]  for x in objData.data]

Aperçu




  • Un autre exemple un peu plus simple
code IronPython

import clr	
import sys
import System
from System.Collections.Generic import List
from System.Collections.ObjectModel import ObservableCollection
from System.Threading import Thread, ThreadStart, ApartmentState
sys.path.append(r'C:\Program Files (x86)\IronPython 2.7\Lib')
sys.path.append(r'C:\Program Files (x86)\IronPython 2.7\DLLs')

try:
	clr.AddReference("IronPython.Wpf")
	clr.AddReference('System.Core')
	clr.AddReference('System.Xml')
	clr.AddReference('PresentationCore')
	clr.AddReference('PresentationFramework')
	clr.AddReferenceByPartialName("WindowsBase")
except IOError:
	raise
	
from System.IO import StringReader
from System.Windows.Markup import XamlReader, XamlWriter
from System.Windows import Window, Application
from System.ComponentModel import INotifyPropertyChanged, PropertyChangedEventArgs

try:
	import wpf
except ImportError:
	raise
	
class XamlLoader(Window):
    LAYOUT = '''
        <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"
            xmlns:local="clr-namespace:WpfApplication1"
            mc:Ignorable="d" 
            Height="600" 
            Width="600" 
            ResizeMode="NoResize"
            Title="A" 
            WindowStartupLocation="CenterScreen" 
            Topmost="True" 
            SizeToContent="Width">
            <Grid Margin="10,0,10,10">
                <Label x:Name="selection_label" Content="Select Item" HorizontalAlignment="Left" Height="30"
                    VerticalAlignment="Top"/>
                <Button x:Name="button_select" Content="Select" HorizontalAlignment="Center" Height="26" Margin="0,63,0,0" VerticalAlignment="Bottom" Width="200" Click="ButtonClick"/>
                <DataGrid x:Name="dataGrid" 
                           AutoGenerateColumns="False"
                           Margin="10,30,10,30"
                           BorderThickness="1"
                           RowHeaderWidth="0"
                           CanUserSortColumns="True"
                           CanUserResizeColumns="False"
                           VerticalScrollBarVisibility="Auto"
                           ItemsSource="{Binding}">
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="Key" Binding="{Binding Key}" IsReadOnly="True" Width="250"/>
                        <DataGridTemplateColumn Header="Param">
                            <DataGridTemplateColumn.CellTemplate>
                                <DataTemplate>
                                    <ComboBox ItemsSource="{Binding LstValue}" SelectedItem="{Binding SelectValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="200"/>
                                </DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
                    </DataGrid.Columns>
                </DataGrid>
            </Grid>
        </Window>'''

    
    def __init__(self, dataCollection):
        self.ui = wpf.LoadComponent(self, StringReader(XamlLoader.LAYOUT))
        self.ui.Title = "Select Types"
        self.data = dataCollection
        self.ui.DataContext = self.data
        
    def ButtonClick(self, sender, e):
        self.DialogResult = True
        self.Close()


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 MyDataViewModel(ViewModelBase):
    def __init__(self):
        ViewModelBase.__init__(self)
        self._Key = None
        # define a variable to store the selected value from conbobox (Binding)
        self._SelectValue = "" 
        self._LstValue = ObservableCollection[System.String]()
    
    # Define all getters and setters properties and Raise the PropertyChanged event with the name of the changed property (setter)
    @property
    def Key(self):
        return self._Key

    @Key.setter
    def Key(self, value):
        self._Key = value
        self.RaisePropertyChanged("Key")
        
    @property
    def SelectValue(self):
        return self._SelectValue

    @SelectValue.setter
    def SelectValue(self, value):
        self._SelectValue = value
        self.RaisePropertyChanged("SelectValue")
        
    @property
    def LstValue(self):
        return self._LstValue
        
    @LstValue.setter
    def LstValue(self, lst_value):
        self._LstValue = ObservableCollection[System.String](lst_value)
        self.RaisePropertyChanged("LstValue")
        
def appThread():
    appThread.form = None
    keys = ["Type1","Type2","Type3","Type4"]
    lst_Mtrl = ["a","b","c","d","e","f"]
    #
    # Create an ObservableCollection of MyDataViewModel objects
    data = ObservableCollection[MyDataViewModel]()
    for key_ in keys:
        en = MyDataViewModel()
        en.Key = key_
        en.LstValue = lst_Mtrl
        data.Add(en)

    xaml = XamlLoader(data)
    # update function attribute
    appThread.form = xaml
    xaml.ui.ShowDialog()
    
if System.Diagnostics.Process.GetCurrentProcess().ProcessName == "DynamoSandbox":
	Application.Current.Dispatcher.Invoke(appThread)
else:
	appThread()

OUT = [[data.Key, data.SelectValue] for data in appThread.form.data]






Ressources


..

0 commentaires:

Enregistrer un commentaire