I want to create a 2 level treview, with the root element of a string "Items":
-Items
-item1
-item2
-...
I have 2 classes to achieve this: ItemList, Item. I call them with the property CurrentItems.
private ItemList _currentItems = null;
public ItemList CurrentItems
{
get
{
return _currentItems;
}
set
{
if(_currentItems!=value)
{
SetProperty(ref _currentItems,
value, () => CurrentItems);
}
}
}
CurrentItems is initialized by creating a temporary ItemsList which is then filled with Items:
ItemList itemTempList = new ItemList();
...
while(...)
{
Item item = new Item();
...
itemTempList.Items.Add(item);
}
CurrentItems = itemTempList;
These are the two classes used:
public class Item
{
public long ObjectID { get; set; }
public string ItemName { get; set; }
}
public class ItemList : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<Item> items = new ObservableCollection<Item>();
public ObservableCollection<Item> Items
{
get
{
return items;
}
set
{
items = value;
OnPropertyChanged();
}
}
private ObservableCollection<string> types = new ObservableCollection<string>();
public ObservableCollection<string> Types
{
get
{
if(types.Count > 0)
{
types.Add("Items");
}
return types;
}
set
{
types = value;
OnPropertyChanged();
}
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
The View is initialized with a constructor:
public ItemView()
{
InitializeComponent();
}
And finally, this is the xaml:
<TreeView Name="itemTree" Grid.Row="1" Grid.Column="0" SelectedItemChanged="itemTree_SelectedItemChanged"
ItemsSource="{Binding CurrentItems}">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="True"/>
</Style>
</TreeView.Resources>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Items,UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}">
<Label Content="{Binding Types,UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}"/>
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ItemName,UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}">
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="Duplicate" Click="DuplicateCell"/>
<MenuItem Header="Delete" Click="DeleteCell"/>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
With my current understanding and code I am not able to create the TreeView.
Any help would be greatly appreciated!
This question already has answers here:
How to use DataTemplateSelector with ContentControl to display different controls based on the view-model?
(2 answers)
Closed 4 years ago.
I'm trying to write a simple dialog that would accept a value in a SpinEdit or a text in a TextEdit. I'm using multiple VMs and I made a selector that should view a proper control based on the logic in the c++/cli file.
XAML:
xmlns:local="clr-namespace:asd"
Title="{Binding Path=Title, Mode=OneTime}"
<dx:DXWindow.Resources>
<DataTemplate x:Key="TInputValueVM" DataType="{x:Type local:TInputValueVM}">
<dxe:SpinEdit Height="23" Width="200"
Text="{Binding Value, Mode=TwoWay}"
Mask="{Binding Mask, Mode=OneWay}"
MaxLength="{Binding Path=InputLength}" />
</DataTemplate>
<DataTemplate x:Key="TInputTextVM" DataType="{x:Type local:TInputTextVM}">
<dxe:TextEdit Height="23" Width="200"
Text="{Binding Value, Mode=TwoWay}"
MaskType="RegEx" Mask="{Binding Mask, Mode=OneWay}"
MaxLength="{Binding Path=InputLength}"/>
</DataTemplate>
<local:PropertyDataTemplateSelector x:Key="templateSelector"
DataTemplate_Value="{StaticResource TInputValueVM}"
DataTemplate_Text="{StaticResource TInputTextVM}" />
</dx:DXWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" >
<Label x:Uid="Label" MinHeight="24" MinWidth="60" Content="Value" />
<ContentControl Content="{Binding Path=Whoami}" ContentTemplateSelector="{StaticResource templateSelector}" />
</StackPanel>
<StackPanel Grid.Row="1" x:Uid="OKCancel_Buttons" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<Button Height="23" x:Name="OK_Button" Click="OK_Click" Content="OK" IsDefault="True" HorizontalAlignment="Right" MinWidth="95" />
<Button Height="23" x:Name="Cancel_Button" Click="Cancel_Click" Content="Cancel" HorizontalAlignment="Right" MinWidth="95" />
</StackPanel>
</Grid>
In c# I have a base VM and two VMS that extend it, one for values and one for text. The rest of the properties stay the same.
C#
namespace asd
{
public class TInputBaseVM : ViewModelBase
{
private string m_sTitle;
private string m_sMask;
private int m_nInputLenght;
private string m_sWhoami;
public TInputBaseVM(string A_sTitle, string A_sMask, int A_nInputLength)
{
m_sTitle = A_sTitle;
m_sMask = A_sMask;
m_nInputLenght = A_nInputLength;
}
protected string Title
{
get { return m_sTitle; }
set { SetProperty(ref m_sTitle, value, () => Title); }
}
protected string Mask
{
get { return m_sMask; }
set { SetProperty(ref m_sMask, value, () => Mask); }
}
protected int InputLength
{
get { return m_nInputLenght; }
set { SetProperty(ref m_nInputLenght, value, () => InputLength); }
}
protected string Whoami
{
get { return m_sWhoami; }
set { SetProperty(ref m_sWhoami, value, () => Whoami); }
}
}
public class TInputValueVM : TInputBaseVM
{
public TInputValueVM(string A_sTitle, string A_sMask, int A_nInputLength, double A_nValue) : base(A_sTitle, A_sMask, A_nInputLength)
{
Value = A_nValue;
Whoami = "Value";
}
private double m_nValue;
public double Value
{
get { return m_nValue; }
set { SetProperty(ref m_nValue, value, () => Value); }
}
}
public class TInputTextVM : TInputBaseVM
{
public TInputTextVM(string A_sTitle, string A_sMask, int A_nInputLength, string A_sValue) : base(A_sTitle, A_sMask, A_nInputLength)
{
Value = A_sValue;
Whoami = "Text";
}
private string m_sValue;
public string Value
{
get { return m_sValue; }
set { SetProperty(ref m_sValue, value, () => Value); }
}
}
public class PropertyDataTemplateSelector : DataTemplateSelector
{
public DataTemplate DataTemplate_Value { get; set; }
public DataTemplate DataTemplate_Text { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var selector = item as string;
if(selector == "Value")
return DataTemplate_Value;
return DataTemplate_Text;
}
}
}
In c++/cli I create an object of a proper VM and I'd like the WPF to automatically update the view to either spinedit or textedit, however I'm not sure how to properly bind the properties from the C#. If I explicitly type 'Value' in the Content property of the ContentControl then it displays the spinEdit but I don't know how to bind it so it automatically takes the correct property.
EDIT: I'm adding c++/cli code to show how I choose different VMs
C++/cli:
bool TSignalNumberPositionDialogCLR::StartDialog(TSignalNumberPositionSupport& A_Attributes, HWND A_hwndParent, LPTSTR String)
{
try
{
TInputValueVM ^oExchange_Value;
TInputTextVM ^oExchange_Text;
int inputFormat = A_Attributes.GetInputFormat();
if(inputFormat)
oExchange_Text = gcnew TInputTextVM(gcnew System::String(A_Attributes.GetTitle()), gcnew System::String(A_Attributes.GetMask()),
A_Attributes.GetInputLength(), gcnew System::String(A_Attributes.GetInitialText()));
else
oExchange_Value = gcnew TInputValueVM(gcnew System::String(A_Attributes.GetTitle()), gcnew System::String(A_Attributes.GetMask()),
A_Attributes.GetInputLength(), A_Attributes.GetInitialValue());
Dialogs::TSignalNumberPositionDialog^ dialog = gcnew Dialogs::TSignalNumberPositionDialog();
if(inputFormat)
dialog->DataContext = oExchange_Text;
else
dialog->DataContext = oExchange_Value;
dialog->ShowDialog();
if(dialog->DialogResult)
{
CString nValue;
if(inputFormat)
nValue = oExchange_Text->Value;
else
nValue = ((Decimal)oExchange_Value->Value).ToString("F2", CultureInfo::InvariantCulture);
A_Attributes.UpdateValue(nValue, String, A_Attributes.GetInputLength());
return true;
}
return false;
}
catch(Exception^ e)
{
e;
}
}
based on the 'inputFormat' variable I want to display different controls in the dialog.
EDIT: Based on #Clemens comments I got rid of the selector sectionand the x:Key property in the DataTemplates. I changed the content opf the Content property to Content="{Binding}" and it somehow works. The moment I create a VM it selects the correct one.
Based on your comment. Let me improve my answer. As you are facing issue in VM selection. so plesae concentrate how I assigned VM to datatemplate. Although it is done in very basic way, you can handle it if you you are using MVVM packages.
I have created 2 data template and 2 vms and each vm is bound to datatemplate. To verify, I have a combobox, which will select datatemplate based on selected value.
Here is Sample VM
public class VM : System.ComponentModel.INotifyPropertyChanged
{
private string title;
private SolidColorBrush background;
public string Title { get => title; set { title = value; RaisePropertyChanged(); } }
public SolidColorBrush Background { get => background; set { background = value; RaisePropertyChanged(); } }
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
public class VM1: VM
{
public VM1()
{
Title = "This is VM1";
Background = Brushes.Yellow;
}
}
public class VM2: VM
{
public VM2()
{
Title = "This is VM2";
Background = Brushes.Orange;
}
}
Now check for resources
<local:VM1 x:Key="VM1"/>
<local:VM2 x:Key="VM2"/>
<DataTemplate x:Key="DT1">
<Grid DataContext="{StaticResource VM1}">
<TextBlock Text="{Binding Title}" Background="{Binding Background}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="DT2">
<Grid DataContext="{StaticResource VM2}">
<TextBlock Text="{Binding Title}" Background="{Binding Background}"/>
</Grid>
</DataTemplate>
<Style TargetType="ContentControl" x:Key="contentStyle">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=cmbo, Path=SelectedValue}" Value="Template1">
<Setter Property="ContentTemplate" Value="{StaticResource DT1}" />
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=cmbo, Path=SelectedValue}" Value="Template2">
<Setter Property="ContentTemplate" Value="{StaticResource DT2}" />
</DataTrigger>
</Style.Triggers>
</Style>
and finally I have combobox and content control just to verify
<ComboBox Name="cmbo"/>
<ContentControl Style="{StaticResource contentStyle}"/>
where cmbo.ItemsSource = new List { "Template1", "Template2" };
Hope you got the point
I have a custom column header where each column's header has TextBox which contains name of the column and ComboBox, which contains information about the type of the column, e.g. "Date", "Number", etc.
I'm trying to bind ComboBox and keep its value somewhere, so that when user selects new value from ComboBox I can recreate table with the column's type changed. Basically all I need is to store somehow each ComboBox's value in a list somehow. I want to do the same with TextBox which should contain name of the column.
This is what I have so far.
<DataGrid x:Name="SampleGrid" Grid.Column="0" Grid.Row="3" Grid.ColumnSpan="2" ItemsSource="{Binding SampledData}">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding ., Mode=OneWay}"/>
<ComboBox>
// How can I set it from ViewModel?
<ComboBoxItem Content="Date"></ComboBoxItem>
<ComboBoxItem Content="Number"></ComboBoxItem>
</ComboBox>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.Resources>
</DataGrid>
ViewModel:
private DataTable _sampledData = new DataTable();
public DataTable SampledData
{
get => _sampledData;
set { _sampledData = value; NotifyOfPropertyChange(() => SampledData); }
}
Solutions in code behind are welcome too as long as I can pass the mappings to ViewModel later.
EDIT:
I've been trying to make this work with a List of ViewModels, but no luck:
public class ShellViewModel : Screen
{
public List<MyRowViewModel> Rows { get; set; }
public ShellViewModel()
{
Rows = new List<MyRowViewModel>
{
new MyRowViewModel { Column1 = "Test 1", Column2= 1 },
new MyRowViewModel { Column1 = "Test 2", Column2= 2 }
};
}
}
View
<DataGrid ItemsSource="{Binding Rows}">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding ., Mode=OneWay}"/>
<ComboBox ItemsSource="{Binding ??????}" />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.Resources>
</DataGrid>
Row
public class MyRowViewModel : PropertyChangedBase
{
public string Column1 { get; set; }
public int Column2 { get; set; }
}
EDIT2:
To clarify, I need a solution that will handle dynamic number of columns, so some files may store 3 columns and some might store 40 columns. I use this for parsing csv files to later display the data. In order to do that I have to know what types of values the file contains. Because some types may be ambiguous, I let the user decide which types they want. This is identical to Excel's "Load From File" wizard.
The wizard loads a small chunk of data (100 records) and allows user to decide what type the columns are. It automatically parses the columns to:
Let user see how the data will look like
Validate if the column can actually be parsed (e.g. 68.35 cannot
be parsed as DateTime)
Another thing is naming each column. Someone might load csv with each column named C1, C2... but they want to assign meaningful names such as Temperature, Average. This of course has to be parsed later too, because two columns cannot have the same name, but I can take care of this once I have a bindable DataGrid.
Let's break your problem into parts and solve each part separately.
First, the DataGrid itemsource, to make things easier, let's say that our DataGrid has only two columns, column 1 and column 2. A basic model for the DataGrid Items should looks like this:
public class DataGridModel
{
public string FirstProperty { get; set; }
public string SecondProperty { get; set; }
}
Now, assuming that you have a MainWindow (with a ViewModel or the DataContext set to code behind) with a DataGrid in it , let's define DataGridCollection as its ItemSource:
private ObservableCollection<DataGridModel> _dataGridCollection=new ObservableCollection<DataGridModel>()
{
new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"},
new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"},
new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"}
};
public ObservableCollection<DataGridModel> DataGridCollection
{
get { return _dataGridCollection; }
set
{
if (Equals(value, _dataGridCollection)) return;
_dataGridCollection = value;
OnPropertyChanged();
}
}
Second, now the interesting part, the columns structure. Let's define a model for your DataGrid's columns, the model will hold all the required properties to set your DataGrid columns, including:
-DataTypesCollection: a collection that holds the combobox itemsource.
-HeaderPropertyCollection: a collection of Tuples, each Tuple represent a column name and a data type, the data type is basically the selected item of column's combobox.
public class DataGridColumnsModel:INotifyPropertyChanged
{
private ObservableCollection<string> _dataTypesCollection = new ObservableCollection<string>()
{
"Date","String","Number"
};
public ObservableCollection<string> DataTypesCollection
{
get { return _dataTypesCollection; }
set
{
if (Equals(value, _dataTypesCollection)) return;
_dataTypesCollection = value;
OnPropertyChanged();
}
}
private ObservableCollection<Tuple<string, string>> _headerPropertiesCollection=new ObservableCollection<Tuple<string, string>>()
{
new Tuple<string, string>("Column 1", "Date"),
new Tuple<string, string>("Column 2", "String")
}; //The Dictionary has a PropertyName (Item1), and a PropertyDataType (Item2)
public ObservableCollection<Tuple<string,string>> HeaderPropertyCollection
{
get { return _headerPropertiesCollection; }
set
{
if (Equals(value, _headerPropertiesCollection)) return;
_headerPropertiesCollection = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Now in you MainWindow's viewmodel (or codebehind) define an instance of the DataGridColumnsModel that we will be using to hold our DataGrid structure:
private DataGridColumnsModel _dataGridColumnsModel=new DataGridColumnsModel();
public DataGridColumnsModel DataGridColumnsModel
{
get { return _dataGridColumnsModel; }
set
{
if (Equals(value, _dataGridColumnsModel)) return;
_dataGridColumnsModel = value;
OnPropertyChanged();
}
}
Third, getting the column's TextBox's value. For that w'll be using a MultiBinding and a MultiValueConverter, the first property that w'll be passing to the MultiBinding is the collection of tuples that we define (columns' names and datatypes): HeaderPropertyCollection, the second one is the current column index that w'll retrieve from DisplayIndex using an ancestor binding to the DataGridColumnHeader:
<TextBox >
<TextBox.Text>
<MultiBinding Converter="{StaticResource GetPropertConverter}">
<Binding RelativeSource="{RelativeSource AncestorType={x:Type Window}}" Path="DataGridColumnsModel.HeaderPropertyCollection"/>
<Binding Path="DisplayIndex" Mode="OneWay" RelativeSource="{RelativeSource RelativeSource={x:Type DataGridColumnHeader}}"/>
</MultiBinding>
</TextBox.Text>
The converter will simply retrieve the right item using the index from collection of tuples:
public class GetPropertConverter:IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
try
{
var theCollection = values[0] as ObservableCollection<Tuple<string, string>>;
return (theCollection?[(int)values[1]])?.Item1; //Item1 is the column name, Item2 is the column's ocmbobox's selectedItem
}
catch (Exception)
{
//use a better implementation!
return null;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Fourth, The last part is to update the DataGrid's ItemSource when the Combobox's selection changed, for that you could use the Interaction tools defined in System.Windows.Interactivity namespace (which is part of Expression.Blend.Sdk, use NuGet to install it: Install-Package Expression.Blend.Sdk):
<ComboBox ItemsSource="{Binding DataGridColumnsModel.DataTypesCollection,RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding UpdateItemSourceCommand,RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
Each time the selectionChanged event occurred, update your DataGrid's ItemSource in the UpdateItemSourceCommand that should be added to your mainWindow's ViewModel:
private RelayCommand _updateItemSourceCommand;
public RelayCommand UpdateItemSourceCommand
{
get
{
return _updateItemSourceCommand
?? (_updateItemSourceCommand = new RelayCommand(
() =>
{
//Update your DataGridCollection, you could also pass a parameter and use it.
//Update your DataGridCollection based on DataGridColumnsModel.HeaderPropertyCollection
}));
}
}
Ps: the RelayCommand class i am using is part of GalaSoft.MvvmLight.Command namespace, you could add it via NuGet, or define your own command.
Finally here the full xaml code:
Window x:Class="WpfApp1.MainWindow"
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:WpfApp1"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
<local:GetPropertConverter x:Key="GetPropertConverter"/>
</Window.Resources>
<Grid>
<DataGrid x:Name="SampleGrid" ItemsSource="{Binding DataGridCollection}" AutoGenerateColumns="False">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel>
<TextBox >
<TextBox.Text>
<MultiBinding Converter="{StaticResource GetPropertConverter}">
<Binding RelativeSource="{RelativeSource AncestorType={x:Type Window}}" Path="DataGridColumnsModel.HeaderPropertyCollection"/>
<Binding Path="DisplayIndex" Mode="OneWay" RelativeSource="{RelativeSource AncestorType={x:Type DataGridColumnHeader}}"/>
</MultiBinding>
</TextBox.Text>
</TextBox>
<ComboBox ItemsSource="{Binding DataGridColumnsModel.DataTypesCollection,RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding UpdateItemSourceCommand,RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="First Column" Binding="{Binding FirstProperty}" />
<DataGridTextColumn Header="Second Column" Binding="{Binding SecondProperty}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
And view models / codebehind:
public class GetPropertConverter:IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
try
{
var theCollection = values[0] as ObservableCollection<Tuple<string, string>>;
return (theCollection?[(int)values[1]])?.Item1; //Item1 is the column name, Item2 is the column's ocmbobox's selectedItem
}
catch (Exception)
{
//use a better implementation!
return null;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class DataGridColumnsModel:INotifyPropertyChanged
{
private ObservableCollection<string> _dataTypesCollection = new ObservableCollection<string>()
{
"Date","String","Number"
};
public ObservableCollection<string> DataTypesCollection
{
get { return _dataTypesCollection; }
set
{
if (Equals(value, _dataTypesCollection)) return;
_dataTypesCollection = value;
OnPropertyChanged();
}
}
private ObservableCollection<Tuple<string, string>> _headerPropertiesCollection=new ObservableCollection<Tuple<string, string>>()
{
new Tuple<string, string>("Column 1", "Date"),
new Tuple<string, string>("Column 2", "String")
}; //The Dictionary has a PropertyName (Item1), and a PropertyDataType (Item2)
public ObservableCollection<Tuple<string,string>> HeaderPropertyCollection
{
get { return _headerPropertiesCollection; }
set
{
if (Equals(value, _headerPropertiesCollection)) return;
_headerPropertiesCollection = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class DataGridModel
{
public string FirstProperty { get; set; }
public string SecondProperty { get; set; }
}
public partial class MainWindow : Window,INotifyPropertyChanged
{
private RelayCommand _updateItemSourceCommand;
public RelayCommand UpdateItemSourceCommand
{
get
{
return _updateItemSourceCommand
?? (_updateItemSourceCommand = new RelayCommand(
() =>
{
//Update your DataGridCollection, you could also pass a parameter and use it.
MessageBox.Show("Update has ocured");
}));
}
}
private ObservableCollection<DataGridModel> _dataGridCollection=new ObservableCollection<DataGridModel>()
{
new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"},
new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"},
new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"}
};
public ObservableCollection<DataGridModel> DataGridCollection
{
get { return _dataGridCollection; }
set
{
if (Equals(value, _dataGridCollection)) return;
_dataGridCollection = value;
OnPropertyChanged();
}
}
private DataGridColumnsModel _dataGridColumnsModel=new DataGridColumnsModel();
public DataGridColumnsModel DataGridColumnsModel
{
get { return _dataGridColumnsModel; }
set
{
if (Equals(value, _dataGridColumnsModel)) return;
_dataGridColumnsModel = value;
OnPropertyChanged();
}
}
public MainWindow()
{
InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Result:
Update
You will achieve the same result by setting AutoGenerateColumns="True" and creating you columns dynamically.
This is not exactly a complete answer but more a hint towards what I think your looking to do, if so you can query me for additional information.
I think what you want to do is define let say a DataGridColumDef type such as this:
public class DataGridColumnDef : NotifyPropertyChangeModel
{
public string Name
{
get => _Name;
set => SetValue(ref _Name, value);
}
private string _Name;
public Type DataType
{
get => _DataType;
set => SetValue(ref _DataType, value);
}
private Type _DataType;
public DataGridColumnDef(string name, Type type)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
DataType = type ?? throw new ArgumentNullException(nameof(type));
}
}
Then I imagine your view model acting as the data context for the DataGrid could look something like this:
public class MainViewModel : NotifyPropertyChangeModel
{
public ObservableList<DataGridColumnDef> ColumnDefinitions
{
get => _ColumnDefinitions;
set => SetValue(ref _ColumnDefinitions, value);
}
private ObservableList<DataGridColumnDef> _ColumnDefinitions;
public ObservableList<DataGridRowDef> RowDefinitions
{
get => _RowDefinitions;
set => SetValue(ref _RowDefinitions, value);
}
private ObservableList<DataGridRowDef> _RowDefinitions;
public MainViewModel()
{
// Define initial columns
ColumnDefinitions = new ObservableList<DataGridColumnDef>()
{
new DataGridColumnDef("Column 1", typeof(string)),
new DataGridColumnDef("Column 2", typeof(int)),
};
// Create row models from initial column definitions
RowDefinitions = new ObservableList<DataGridRowDef>();
for(int i = 0; i < 100; ++i)
{
RowDefinitions.Add(new DataGridRowDef(ColumnDefinitions));
// OR
//RowDefinitions.Add(new DataGridRowDef(ColumnDefinitions, new object[] { "default", 10 }));
}
}
}
This way on the main view model you could subscribe to collection/property changed events in the ColumnDefinitions property and then re-create the rows collection.
Now the trick that I am not 100% sure would work, but not sure why it wouldn't, is making your DataGridRowDef type inherit from DynamicObject so you can spoof members and their values, something like this:
public class DataGridRowDef : DynamicObject
{
private readonly object[] _columnData;
private readonly IList<DataGridColumnDef> _columns;
public static object GetDefault(Type type)
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
return null;
}
public override IEnumerable<string> GetDynamicMemberNames()
{
return _columns.Select(c => c.Name).Union(base.GetDynamicMemberNames());
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
var columnNames = _columns.Select(c => c.Name).ToList();
if(columnNames.Contains(binder.Name))
{
var columnIndex = columnNames.IndexOf(binder.Name);
result = _columnData[columnIndex];
return true;
}
return base.TryGetMember(binder, out result);
}
public DataGridRowDef(IEnumerable<DataGridColumnDef> columns, object[] columnData = null)
{
_columns = columns.ToList() ?? throw new ArgumentNullException(nameof(columns));
if (columnData == null)
{
_columnData = new object[_columns.Count()];
for (int i = 0; i < _columns.Count(); ++i)
{
_columnData[i] = GetDefault(_columns[i].DataType);
}
}
else
{
_columnData = columnData;
}
}
}
Anyway if this kind of solution seems approachable to you I can try and work through it a bit more possibly.
Try this.
Window1.xaml
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:this="clr-namespace:WpfApplication1"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<this:RowDataConverter x:Key="RowDataConverter1" />
</Window.Resources>
<Grid>
<DataGrid ItemsSource="{Binding Rows, Mode=OneWay}">
<DataGrid.Columns>
<DataGridTextColumn>
<DataGridTextColumn.Binding>
<MultiBinding Converter="{StaticResource RowDataConverter1}">
<Binding Path="Column1" Mode="OneWay" />
<Binding Path="Column1OptionString" Mode="OneWay" RelativeSource="{RelativeSource AncestorType=Window, Mode=FindAncestor}" />
</MultiBinding>
</DataGridTextColumn.Binding>
<DataGridTextColumn.HeaderTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="Column Header 1" />
<ComboBox ItemsSource="{Binding ColumnOptions, Mode=OneWay, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
SelectedValue="{Binding Column1OptionString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
SelectedValuePath="Option">
<ComboBox.ItemTemplate>
<DataTemplate DataType="this:ColumnOption">
<TextBlock Text="{Binding Option, Mode=OneTime}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</DataTemplate>
</DataGridTextColumn.HeaderTemplate>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Window1.xaml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
namespace WpfApplication1
{
public partial class Window1 : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public List<RowData> Rows { get; set; }
public List<ColumnOption> ColumnOptions { get; set; }
private string _column1OptionString;
public string Column1OptionString
{
get
{
return _column1OptionString;
}
set
{
_column1OptionString = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Column1OptionString"));
}
}
public Window1()
{
InitializeComponent();
ColumnOptions = new List<ColumnOption>() {
new ColumnOption(){ Option = "String", StringFormat = "" },
new ColumnOption(){ Option = "Int32", StringFormat = "" }
};
Rows = new List<RowData>() {
new RowData(){ Column1 = "01234" }
};
_column1OptionString = "String";
this.DataContext = this;
}
}
public class ColumnOption
{
public string Option { get; set; }
public string StringFormat { get; set; }
}
public class RowData : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private object _column1;
public object Column1
{
get
{
return _column1;
}
set
{
_column1 = value;
if (PropertyChanged!= null)
PropertyChanged(this, new PropertyChangedEventArgs("Column1"));
}
}
}
public class RowDataConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values[1] == null)
return values[0].ToString();
switch (values[1].ToString())
{
case "String":
return values[0].ToString();
case "Int32":
Int32 valueInt;
Int32.TryParse(values[0].ToString(), out valueInt);
return valueInt.ToString();
default:
return values[0].ToString();
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
UPDATE
based on #FCin comment
"This is nice, but I use this to load csv files and the number of columns changes depending of csv file. Here I have to hardcore each column, but some files may have 1 column and some might have 30 columns".
Assume your csv file using format:
line1: Headers,
line2: Data Type,
line3-end: Records.
Example data1.csv:
ColumnHeader1,ColumnHeader2
Int32,String
1,"A"
2,"B"
3,"C"
I try to parse csv file using TextFieldParser, then generate the DataGrid's columns programmatically.
Window2.xaml
<Window x:Class="WpfApplication1.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window2" Height="300" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<Label Content="File:" />
<ComboBox x:Name="FileOption"
SelectionChanged="FileOption_SelectionChanged">
<ComboBox.Items>
<Run Text="Data1.csv" />
<Run Text="Data2.csv" />
</ComboBox.Items>
</ComboBox>
</StackPanel>
<DataGrid x:Name="DataGrid1" Grid.Row="1"
AutoGenerateColumns="False"
ItemsSource="{Binding ListOfRecords, Mode=OneWay}">
</DataGrid>
</Grid>
</Window>
Window2.xaml.cs
using Microsoft.VisualBasic.FileIO;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
namespace WpfApplication1
{
public partial class Window2 : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
List<myDynamicObject> _listOfRecords;
public List<myDynamicObject> ListOfRecords
{
get
{
return _listOfRecords;
}
}
public Window2()
{
InitializeComponent();
DataContext = this;
}
public void LoadData(string fileName)
{
_listOfRecords = new List<myDynamicObject>();
myDynamicObject record;
TextFieldParser textFieldParser = new TextFieldParser(fileName);
textFieldParser.TextFieldType = FieldType.Delimited;
textFieldParser.SetDelimiters(",");
string[] headers = null;
string[] dataTypes = null;
string[] fields;
int i = 0;
while(!textFieldParser.EndOfData)
{
fields = textFieldParser.ReadFields();
if (i == 0)
{
headers = fields;
}
else if (i == 1)
{
dataTypes = fields;
}
else
{
record = new myDynamicObject();
for (int j = 0; j < fields.Length; j++)
{
switch(dataTypes[j].ToLower())
{
case "string":
record.SetMember(headers[j], fields[j]);
break;
case "int32":
Int32 data;
if (Int32.TryParse(fields[j], out data))
{
record.SetMember(headers[j], data);
}
break;
default:
record.SetMember(headers[j], fields[j]);
break;
}
}
_listOfRecords.Add(record);
}
i += 1;
}
PropertyChanged(this, new PropertyChangedEventArgs("ListOfRecords"));
DataGrid1.Columns.Clear();
for (int j = 0; j < headers.Length; j++)
{
DataGrid1.Columns.Add(new DataGridTextColumn()
{
Header = headers[j],
Binding = new Binding()
{
Path = new PropertyPath(headers[j]),
Mode = BindingMode.OneWay
}
});
}
}
private void FileOption_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
LoadData((FileOption.SelectedItem as Run).Text);
}
}
public class myDynamicObject : DynamicObject
{
Dictionary<string, object> dictionary = new Dictionary<string, object>();
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
string name = binder.Name;
return dictionary.TryGetValue(name, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
dictionary[binder.Name] = value;
return true;
}
public void SetMember(string propertyName, object value)
{
dictionary[propertyName] = value;
}
}
}
I have a Button which shows a ContextMenu look like
Here the XAML:
<Window.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
<Style x:Key="MenuItemStyle" TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding Data.OnSelected, Source={StaticResource proxy}}" />
</Style>
<Button Name="ButtonMenu_Export"
Click="ButtonMenu_Export_Click"
Visibility="{Binding ButtonExportEnabled,
Converter={StaticResource VisibilityConverter}}">
<StackPanel Orientation="Vertical">
<Image Source="...." />
<TextBlock Width="70" TextAlignment="Center" Text="Export" />
</StackPanel>
<Button.ContextMenu>
<ContextMenu ItemsSource="{Binding ExportMenuItems}">
<ContextMenu.ItemTemplate>
<HierarchicalDataTemplate ItemContainerStyle="{StaticResource MenuItemStyle}">
<ContentPresenter Content="{Binding Text}" RecognizesAccessKey="True" />
<HierarchicalDataTemplate.ItemsSource>
<Binding Path="SubItems" />
</HierarchicalDataTemplate.ItemsSource>
</HierarchicalDataTemplate>
</ContextMenu.ItemTemplate>
</ContextMenu>
</Button.ContextMenu>
</Button>
The menu is created at runtime using this List (as in this article)
public System.Collections.Generic.List<MenuItem> ExportMenuItems
{
get { return _menuService.GetParentMenuItems(); }
}
Now, what I cannot do is bind the items to the OnSelected command of MenuItem class.
The class which defines the menu is:
public class MenuItem
{
private string name;
private string text;
private int menuId;
private ICommand onSelected;
private MenuItem parent;
private ObservableCollection<MenuItem> subItems;
public MenuItem(string name, string text, int MenuId)
{
this.menuId = MenuId;
this.name = name;
this.text = text;
this.subItems = new ObservableCollection<MenuItem>();
}
public string Name { get { return this.name; } }
public string Text { get { return this.text; } }
public MenuItem Parent { get { return this.parent; } set { this.parent = value; } }
public ICommand OnSelected
{
get
{
if (this.onSelected == null)
this.onSelected = new MenuCommand(this.ItemSelected, this.ItemCanBeSelected, menuId);
return this.onSelected;
}
}
public ObservableCollection<MenuItem> SubItems
{
get { return this.subItems; }
}
}
I created a proxy class as in this article to made DataContext visible to HierarchicalDataTemplate content but maybe I misunderstood something:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
Where I'm wrong?
Change your command binding
Command="{Binding Data.OnSelectedd,Source={StaticResource proxy}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=MenuItem}"
I have created a ListBox with custom ListBoxItem Template, had bound everything and it worked. When I was still working on my project. I runned the program and that ListBox weren't showing items anymore. Here is my code:
This is ListBox which doesn't show any item:
<ListBox
x:Name="LB_SongList"
HorizontalAlignment="Left"
Height="498"
Margin="0,30,0,0"
VerticalAlignment="Top"
Width="319"
Background="{x:Null}"
BorderBrush="{x:Null}"
Drop="LB_SongList_Drop"
PreviewMouseMove="LB_SongList_PreviewMouseMove"
PreviewMouseDoubleClick="LB_SongList_PreviewMouseDoubleClick"
SelectionChanged="LB_SongList_SelectionChanged"
AllowDrop="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Visible"
SelectionMode="Multiple"
ItemTemplate="{DynamicResource SongTemplate}"/>
Here is my ListBoxItem Template:
<DataTemplate x:Key="SongTemplate">
<Grid Width="302" Height="35">
<Label Content="{Binding SongName}" HorizontalAlignment="Stretch" Margin="4" VerticalAlignment="Stretch" Width="Auto" Height="Auto" Padding="2,0,0,0" VerticalContentAlignment="Center"/>
<Border BorderBrush="#B26A6A6A" BorderThickness="4" HorizontalAlignment="Stretch" Height="35" VerticalAlignment="Top" Width="Auto">
<Border.Style>
<Style>
<Style.Triggers>
<DataTrigger
Binding="{Binding Path=IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}"
Value="False">
<Setter
Property="Border.Visibility"
Value="Collapsed"
/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
</Grid>
</DataTemplate>
And the code:
public partial class MenuHolder : UserControl
{
private List<string> List_SongList;
public List<string> L_SongList
{
get { return List_SongList; }
set
{
_File.ListToObservableCollection_Song(value, O_SongList);
List_SongList = value;
}
}
public List<string> L_PlayLists
{
get
{
Settings.Default.Save();
return Settings.Default.L_PlayLists;
}
set
{
Settings.Default.L_PlayLists = value;
Settings.Default.Save();
}
}
public ObservableCollection<Song> O_SongList = new ObservableCollection<Song>();
public ObservableCollection<string> O_PlayList = new ObservableCollection<string>();
public MenuHolder()
{
InitializeComponent();
LB_SongList.ItemsSource = O_SongList;
LB_PlayList.ItemsSource = O_PlayList;
List<string> temp = new List<string>();
temp.Add("TestSong");
L_SongList = temp;
}
}
public class Song : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
string _songName;
string _songLengh;
public Song(){}
public Song(String _name){ SongName = _name; }
public Song(String _name, String _lengh) { SongName = _name; SongLengh = _lengh; }
public string SongName
{
get { return _songName; }
set { _songName = value; RaisePropertyChanged("SongName"); }
}
public string SongLengh
{
get { return _songLengh; }
set { _songLengh = value; RaisePropertyChanged("SongLengh"); }
}
private void RaisePropertyChanged(string propertyName)
{
// take a copy to prevent thread issues
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And the ListToObservableCollection_Song
public void ListToObservableCollection_Song(List<string> _list, ObservableCollection<Music_Player.Menus.MainMenuObjects.Song> _collection)
{
_collection.Clear();
foreach (string _path in _list)
{
_collection.Add(new Menus.MainMenuObjects.Song(GetSongNameFromPath(_path)));
}
}
The worst thing is that it was working before and in the old version of my program it is still working. Still thinking why it is doing that.