WPF Form - DataGrid with ComboBoxes inside

Hi All.
I’m working on user form with DataGrid Source where every type can be updated with ComboBox

I cant understand why get in OUT not updated data after :cry:

Please help!
Thanks in advance!

Here is code:

import clr	
import sys
import System
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')
except IOError:
	raise
	
from System.IO import StringReader
from System.Windows.Markup import XamlReader, XamlWriter
from System.Windows import Window, Application

from System import Uri
from System.Windows.Media.Imaging import BitmapImage

try:
	import wpf
except ImportError:
	raise
	
from collections import namedtuple
clr.AddReference("System.Drawing")
clr.AddReference("System.Windows")
import System.Drawing
from System.Drawing import *
import System.Windows.Media
import traceback
import System.Windows.Controls as WPFControls
import System.Windows.Data as WPFData

	
class UpdateTypes(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"/>
                            <DataGridTextColumn Header="Value" Binding="{Binding Value}" IsReadOnly="True" Width="100"/>
							<DataGridTemplateColumn Header="Param">
								<DataGridTemplateColumn.CellTemplate>
									<DataTemplate>
										 <ComboBox ItemsSource="{Binding Material}" SelectedItem="{Binding Parameter}" Width="200"/>
								</DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
                        </DataGrid.Columns>
                    </DataGrid>
				</Grid>
			</Window>'''
	
	
	def __init__(self, options):
		self.ui = wpf.LoadComponent(self, StringReader(UpdateTypes.LAYOUT))
		self.ui.Title = "Select Types"
		self.ui.dataGrid.DataContext = options

			
	def ButtonClick(self, sender, e):
		self.selected = self.ui.dataGrid.Items
		self.DialogResult = True
		self.Close()

run = 1
		
keys = ["Type1","Type2","Type3","Type4"]
values = [0, 1, 2, 3]
Param = ["a","b","c","d"]
Materials = [["a","b","c","d","e","f"]]  # Define the source for the Param column ComboBox	

tempList = list(Materials)

for i in range(len(values)):
    for element in tempList:
       MaterialsList = Materials.append(element)

MyImage = namedtuple('MyImage', ['Key', 'Value', 'Parameter', 'Material'])
data = []
 
for key_, value_, ParamStr, Mtrl in zip(keys, values, Param, Materials):
    data.append(MyImage(key_, value_, ParamStr, Mtrl))

if run:
    form = UpdateTypes(data)
    form.ShowDialog()
    OUT = form.selected

else:
    OUT = 'Set RUN to True'
1 Like

You aren’t performing any callbacks to your view so the property never updates.

  1. Use the right tool for the right job: C# in Visual Studio.
  2. Adhere to OOP, and create classes not tuples.
  3. After 2, you’re now able to implement INotifyPropertyChanged and therefore your callbacks.

In VS you have proper type checking, XAML linting (if you use Resharper), a design view which will significantly speed up UI design, and it will allow you to adopt a design pattern, e.g. MVVM, that makes you program structure far easier to reason about and create.

You can still do the above using Python its just going to be more difficult than it needs to be, and if you’re just starting out, you’re heading down a pathway to nowhere as this just isn’t the right language nor development environment to be writing GUI applications with a complex framework like WPF.

5 Likes

Thank you for your responds. I know that Python is not good for such of work but its easy to implement as basic input form for your Script in Dynamo. I dont know if there anyway as well easy to do it with C#
yes, I’m just starting to understand how it works.

Here an another code what I made without XAML inside, and it works. If value of Param_2 is not in collectList return default value but hidden in ComboBox:
image

import clr
import sys
import System
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('System.Core')
    clr.AddReference('PresentationCore')
    clr.AddReference('PresentationFramework')
    clr.AddReference('System.Windows')
    clr.AddReference("WindowsBase")
except IOError:
    raise

from System.Windows import Application, Window, Thickness
from System.Windows.Controls import DataGrid, DataGridTextColumn, DataGridSelectionUnit, Button, Grid, ComboBox, DataGridComboBoxColumn, DataGridLength
from System.Windows.Data import Binding
from System.Windows.Input import MouseAction, MouseButtonEventArgs
from System.Collections.ObjectModel import ObservableCollection

class MyItem(object):
    def __init__(self, column1, param, Param_2):
        self.Column1 = column1
        self.Param = param
        self.Param_2 = Param_2

class MyWindow(Window):
    def __init__(self, inputList):
        self.myItems = ObservableCollection[MyItem]()

        for i in range(len(inputList[0])):
            self.myItems.Add(MyItem(inputList[0][i], inputList[1][i], inputList[2][i]))

        self.dataGrid = DataGrid()
        self.dataGrid.ItemsSource = self.myItems
        self.dataGrid.AutoGenerateColumns = False
        self.dataGrid.FrozenColumnCount = 3
        self.dataGrid.Margin = Thickness(5, 20, 5, 60)
        self.dataGrid.EnableColumnVirtualization = False
        self.dataGrid.SelectionUnit = DataGridSelectionUnit.FullRow
        self.dataGrid.CanUserAddRows = False

        # Define the ComboBox column
        comboBoxColumn = DataGridComboBoxColumn()
        comboBoxColumn.Header = "Param_2"
        comboBoxColumn.ItemsSource = inputList[0]
        comboBoxColumn.DisplayMemberPath = "Column_1"
        comboBoxColumn.SelectedValuePath = "Column_1"
        comboBoxColumn.SelectedValueBinding = Binding("Param_2")

        self.dataGrid.Columns.Add(DataGridTextColumn(Header="Column_1", IsReadOnly = True, Width=DataGridLength(200), Binding=Binding("Column1")))
        self.dataGrid.Columns.Add(DataGridTextColumn(Header="Param", IsReadOnly = True, Width=DataGridLength(80), Binding=Binding("Param")))
        self.dataGrid.Columns.Add(DataGridComboBoxColumn(Header="Param_2", Width=DataGridLength(200), SelectedItemBinding=Binding("Param_2"), ItemsSource=collectList))


        self.dataGrid.LoadingRow += self.dataGrid_LoadingRow

        self.doneButton = Button()
        self.doneButton.Content = "Done"
        self.doneButton.Margin = Thickness(10,420,10,10)
        self.doneButton.Click += self.doneButton_Click

        self.grid = Grid()
        self.grid.Children.Add(self.dataGrid)
        self.grid.Children.Add(self.doneButton)

        self.Content = self.grid
        self.Width = 500
        self.Height = 500
        self.Title = "My Window"
        self.ResizeMode = System.Windows.ResizeMode.NoResize
        self.WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen
        self.ShowDialog()

    def dataGrid_LoadingRow(self, sender, e):
        row = e.Row
        row.PreviewMouseLeftButtonDown += self.row_PreviewMouseLeftButtonDown

    def row_PreviewMouseLeftButtonDown(self, sender, e):
        if e.ClickCount == 2:
            row = sender
            row.IsSelected = True
            self.dataGrid.BeginEdit()
            e.Handled = True

    def doneButton_Click(self, sender, e):
        self.DialogResult = True
        self.Close()

def getUpdatedList(inputList):
    class Binding(object):
        def __init__(self, path):
            self.Path = path


    myWindow = MyWindow(inputList)
    if myWindow.DialogResult == True:
        updatedList = [[item.Column1 for item in myWindow.myItems], [item.Param for item in myWindow.myItems], [item.Param_2 for item in myWindow.myItems]]
        return updatedList
    else:
        return inputList

inputList = [[0, 1, 2, 3], ["a","b","c","d"], ["a1", "b1", "c1", "d1"]]
collectList = ["b1", "c1", "d1", "e1", "f1", "g1", "h1", "i1"]
OUT = getUpdatedList(inputList)

1 Like

Here is an example of implementation using MVVM pattern

MVVM with Ipy

Example MVVM and 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]

Alternatively you can read the contents of the cells but we lose some advantages of wpf and data Binding, it would be better to switch to Winform

By reading content of cells
import clr  
import sys
import System
from System.Collections.Generic import List
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

clr.AddReference('System.Data')
from System.Data import DataTable

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"
                                    CanUserAddRows = "False"
                                    VerticalScrollBarVisibility="Auto"
                                    ItemsSource="{Binding}">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Key" Binding="{Binding Key}" IsReadOnly="True" Width="250"/>
                            <DataGridTemplateColumn Header="Param">
                                <DataGridTemplateColumn.CellTemplate>
                                    <DataTemplate>
                                         <ComboBox x:Name="cbBox" ItemsSource="{Binding Parameter}" 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.selected = []
        self.ui.DataContext = dataCollection
        
    def ButtonClick(self, sender, e):
        for idx, row  in enumerate(self.ui.dataGrid.Items):
            data_row =  self.ui.dataGrid.ItemContainerGenerator.ContainerFromIndex(idx)
            temp = []
            print(data_row)
            for column in self.ui.dataGrid.Columns:
                cellContent = column.GetCellContent(data_row)
                if isinstance(cellContent, System.Windows.Controls.TextBlock):
                    temp.append(cellContent.Text)
                elif isinstance(cellContent, System.Windows.Controls.ContentPresenter):
                    myDataTemplate = cellContent.ContentTemplate
                    myTextBlock = myDataTemplate.FindName("cbBox",  cellContent)
                    temp.append(myTextBlock.Text)
                else:
                    temp.append(None)
            self.selected.append(temp)
        self.DialogResult = True
        self.Close()
       
def appThread():
    appThread.form = None
    keys = ["Type1","Type2","Type3","Type4"]
    lst_Mtrl = List[System.String](["a","b","c","d","e","f"])
    #
    dt = DataTable("MyTable")
    dt.Columns.Add("Key", System.String)
    dt.Columns.Add("Parameter", List[System.String])
    # populate dataTable
    for key_ in keys:
        dt.Rows.Add(key_, lst_Mtrl)

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

OUT = appThread.form.selected
8 Likes