I'm working all day on this, but I'm stuck and don't have a solution. It seems pretty simple..
I have a datagrid with a dynamic amount of columns. Each column has a DataGridTemplateColumn with a CellTemplate and a CellEditingTemplate. The CellEditingTemplate contains a Combobox with values which after the user has selected a value, the TextBox in the CellTemplate has to be updated to the selected value. Normally you would bind this to a known property in an object, but since I don't know how many columns the user needs in advance, I have to create these properties (or something else) at runtime and try to bind this textbox and combobox to this property. How do I solve this? I prefer to not use ExpandoObjects and I'm bounded to .net 4.0
My solution (which didn't work yet) is to add a wrapperobject to a list/array in each row object and then generate a new DataGridTemplateColumn in code where the combobox and the textbox have databindings to this wrapperobject in the array. In XAML this would look something like , but I can't get this 0 changed into 1,2,3,...
I could do this in the code behind, but I don't know how to bind it to a WrapperObjectList[X].Value?
Here is a simplified example, hopefully helping understanding what I'd like to achieve:
I have datagrid which is binded to an observablecollection with RowObjects [rowobject represents 1 row.. ;) ]. The first column is fixed, so it's binded to a property in a RowObject.
Now I have to add a dynamic amount of columns after this. This is based on how many items the user has selected (not in the example, but you'll get the point). I replaced this action with clicking the button.
For each column I create a wrapperobject and add this to another observablecollection within the RowObject. This gives me a bit of flexibility since I don't know in advance how many columns will be added. This Wrapperobject contains the value I need to display.
Furthermore, I need this WrapperObject to be binded to a selection in my combobox. So if I edit a row, I select a value from the combobox and then this selected value has to be displayed (set) in the TextBox of the celltemplate.
Example code:
Code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
LoadData();
this.DataContext = RowList;
}
private void LoadData()
{
RowList = new ObservableCollection<RowObject>();
WrapperObject lWrapperObject1 = new WrapperObject();
lWrapperObject1.Value = 1;
WrapperObject lWrapperObject2 = new WrapperObject();
lWrapperObject2.Value = 2;
WrapperObject lWrapperObject3 = new WrapperObject();
lWrapperObject3.Value = 3;
WrapperObject lWrapperObject4 = new WrapperObject();
lWrapperObject4.Value = 4;
WrapperObject lWrapperObject6 = new WrapperObject();
lWrapperObject6.Value = 6;
WrapperObject lWrapperObject8 = new WrapperObject();
lWrapperObject6.Value = 8;
RowObject lRowObject1 = new RowObject();
lRowObject1.RowObjectName = "RowObject1";
lRowObject1.WrapperCollection.Add(lWrapperObject1);
lRowObject1.WrapperCollection.Add(lWrapperObject2);
lRowObject1.WrapperCollection.Add(lWrapperObject6);
RowObject lRowObject2 = new RowObject();
lRowObject2.RowObjectName = "RowObject2";
lRowObject2.WrapperCollection.Add(lWrapperObject3);
lRowObject2.WrapperCollection.Add(lWrapperObject4);
lRowObject2.WrapperCollection.Add(lWrapperObject8);
RowList.Add(lRowObject1);
RowList.Add(lRowObject2);
}
public ObservableCollection<RowObject> RowList { get; set; }
private void Button_Click(object sender, RoutedEventArgs e)
{
DataGridTemplateColumn lTemplateColumn = new DataGridTemplateColumn();
lTemplateColumn.Header = "NewDynamicColumn";
var lMyCellTemplate= (DataTemplate)Resources["myCellTemplate"];
var lMyCellEditingTemplate = (DataTemplate)Resources["myCellEditingTemplate"];
// Text="{Binding WrapperCollection[1].Value}" ????
lTemplateColumn.CellTemplate =lMyCellTemplate;
lTemplateColumn.CellEditingTemplate = lMyCellEditingTemplate;
this.MyGrid.Columns.Add(lTemplateColumn);
}
}
public class RowObject : INotifyPropertyChanged
{
public RowObject()
{
WrapperCollection = new ObservableCollection<WrapperObject>();
ComboBoxList = new List<string>();
ComboBoxList.Add("10");
ComboBoxList.Add("20");
}
public ObservableCollection<WrapperObject> WrapperCollection { get; set; }
private string fRowObjectName;
public string RowObjectName
{
get { return fRowObjectName; }
set
{
fRowObjectName = value;
RaisePropertyChanged("RowObjectName");
}
}
public List<string> ComboBoxList { get; set; }
#region Notify property changed
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
public class WrapperObject : INotifyPropertyChanged
{
private int fValue;
public int Value
{
get {return fValue;}
set
{
fValue = value;
RaisePropertyChanged("Value");
}
}
#region Notify property changed
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
Xaml:
<Window.Resources>
<DataTemplate x:Key="myCellTemplate">
<TextBlock Text="{Binding WrapperCollection[1].Value}" />
</DataTemplate>
<DataTemplate x:Key="myCellEditingTemplate">
<ComboBox ItemsSource="{Binding ComboBoxList}" />
</DataTemplate>
</Window.Resources>
<StackPanel>
<DataGrid x:Name="MyGrid"
AutoGenerateColumns="False"
ItemsSource="{Binding}"
>
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding RowObjectName}"/>
<DataGridTextColumn Header="ExampleColumn1" Binding="{Binding WrapperCollection[0].Value}"/>
<DataGridTemplateColumn Header="ExampleColumn2" CellTemplate="{StaticResource myCellTemplate}" CellEditingTemplate="{StaticResource myCellEditingTemplate}"/>
</DataGrid.Columns>
</DataGrid>
<Button Content="Click" Click="Button_Click" Width="100" Margin="20"/>
</StackPanel>
ExampleColumn1 and ExampleColumn2 are the only editable columns in this example and once you click the button, the added column(s) are also editable.
Right now I think the solution is something like creating a new binding to the WrapperCollection[2].Value, WrapperCollection[3].Value etc. in the code behind, but how?
The XAMLReader saved me! Now I can create any DataTemplate I'd like to have..
public DataTemplate CreateDataTemplate(Type type, int aNumber)
{
StringReader stringReader = new StringReader(
#"<DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">
<" + type.Name + #" Text=""{Binding WrapperCollection[" + aNumber.ToString() + #"].Value}""/>
</DataTemplate>");
XmlReader xmlReader = XmlReader.Create(stringReader);
return XamlReader.Load(xmlReader) as DataTemplate;
}
{
...
TextBlock textBlock = new TextBlock();
var lMyCellTemplate = CreateDataTemplate(textBlock.GetType(), Counter);
}
Related
I am trying to achieve rather something simple, and I believe that my approach might be wrong. I am creating a datagrid, where the first column has a seperate width from the other ones. I am using AutoGenerateColumns=true, as it simplifies my work. I cannot use pure XAML as I do not know the amount of columns before runtime, and I was not able to connect XAML and AutoGenerateColumns, so it would use the first column's layout, and then generate the rest.
My approaches:
1) Create two data grids next to each other - the issue with that approach is the need to manage 2 seperate datagrids, I saw issues with scrolling and adjusting their sizes, so I decided to change my approach, to keep everyhting within one DataGrid as the data relates to each other.
2) Trying to get the Datagrid object from Code-Behind so I can set the Width property from the ViewModel class, this would break the MVVM model, and also was difficult for my to implement
3) Current approach - using the AutoGeneratingColumn event, I capture the first column and try to bind to its WidthProperties. Unfortunately this does not seem to work, and I do not know why.
This is my Code-Behind file for the XAML containing the DataGrid
private void DG1_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
string headername = e.Column.Header.ToString();
//Cancel the column you don't want to generate
if (headername == "DATE")
{
Binding binding = new Binding("DateColumnWidth");
binding.Source = DataGrid.DataContext; // DataGrid is the name of the DataGrid
binding.Mode = BindingMode.TwoWay;
binding.Path = new PropertyPath("DateColumnWidth");
BindingOperations.SetBinding(e.Column, ColumnDefinition.MinWidthProperty, binding);
BindingOperations.SetBinding(e.Column, ColumnDefinition.MaxWidthProperty, binding);
e.Column.Header = "Test";
}
}
This is my Proprty in the ViewModel. Whilst debugging the binding source, it attaches to the right class and I see all my properties. It also changes the header of the right column.
private int _DateColumnWidth;
public int DateColumnWidth
{
get { return _DateColumnWidth; }
set
{
_DateColumnWidth = value;
RaisePropertyChanged("DateColumnWidth");
}
}
I set the debugger to show me all the data binding tracing information, no problems arise, but the width is not updating.
What am I doing wrong?
I created a mock up based on your code and it worked. Then I looked more closely and realised you have this:
BindingOperations.SetBinding(e.Column, ColumnDefinition.MinWidthProperty, binding);
ColumnDefinition is the wrong object. It should be DataGridColumn:
BindingOperations.SetBinding(e.Column, DataGridColumn.MaxWidthProperty, binding);
Here is my test. The grid's first column is bound to a ColumnWidth property on ViewModel. There is a Slider control below the grid with the same binding. Sliding the slider changes the first column's width.
MainWindow.xaml:
<Window x:Class="SO_59604847_DataGridBoundColumnWidth.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SO_59604847_DataGridBoundColumnWidth"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DataGrid
AutoGenerateColumns="True"
ItemsSource="{Binding GridItems}"
AutoGeneratingColumn="DataGrid_AutoGeneratingColumn">
</DataGrid>
<Slider Grid.Row="1" Minimum="1" Maximum="1000" Value="{Binding ColumnWidth}" />
</Grid>
</Window>
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.Column.Header.ToString() == "ColumnOne")
{
var binding = new Binding("ColumnWidth");
binding.Source = this.DataContext;
BindingOperations.SetBinding(e.Column, DataGridColumn.MinWidthProperty, binding);
BindingOperations.SetBinding(e.Column, DataGridColumn.MaxWidthProperty, binding);
}
}
}
ViewModel.cs:
public class ViewModel : INotifyPropertyChanged
{
private double _columnWidth;
public double ColumnWidth
{
get { return _columnWidth; }
set { _columnWidth = value; FirePropertyChanged(); }
}
private List<GridItem> _gridItems = new List<GridItem>()
{
new GridItem() { ColumnOne = "1.1", ColumnTwo = "1.2", ColumnThree = "1.3" },
new GridItem() { ColumnOne = "2.1", ColumnTwo = "2.2", ColumnThree = "2.3" },
new GridItem() { ColumnOne = "3.1", ColumnTwo = "3.2", ColumnThree = "3.3" }
};
public List<GridItem> GridItems
{
get { return _gridItems; }
}
private void FirePropertyChanged([CallerMemberName] string caller = "")
{
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class GridItem
{
public string ColumnOne { get; set; }
public string ColumnTwo { get; set; }
public string ColumnThree { get; set; }
}
It does not do two-way binding because that doesn't make sense if you're binding to the Min and Max column widths (the user can't change these - and indeed cannot change the width because the Min and Max widths are set to the same value). If your intention was to bind the width two-ways then you will need to bind to the WidthProperty dependency property and not the Min/MaxWidthProperty DPs (though, you may need a value converter then because Width is a GridLength not a plain number. Say so if that is what you were trying to do and I'll see if I can update the answer accordingly.
I have different groups of controls bound to different categories of ViewModel classes.
The ViewModels are
MainViewModel
VideoViewModel
AudioViewModel
Question
How can I set the DataContext with XAML instead of C#?
1. I tried adding DataContext="{Binding VideoViewModel}" to the ComboBox XAML, but it didn't work and the items came up empty.
2. I also tried grouping all the ComboBoxes of a certain category inside a UserControl with the DataContext:
<UserControl DataContext="{Binding VideoViewModel}">
<!-- ComboBoxes in here -->
</UserControl>
3. Also tried setting the <Window> DataContext to itself DataContext="{Binding RelativeSource={RelativeSource Self}}"
Data Context
I'm currently setting the DataContext this way for the different categories of controls:
public MainWindow()
{
InitializeComponent();
// Main
this.DataContext =
tbxInput.DataContext =
tbxOutput.DataContext =
cboPreset.DataContext =
MainViewModel.vm;
// Video
cboVideo_Codec.DataContext =
cboVideo_Quality.DataContext =
tbxVideo_BitRate.DataContext =
cboVideo_Scale.DataContext =
VideoViewModel.vm;
// Audio
cboAudio_Codec.DataContext =
cboAudio_Quality.DataContext =
tbxAudio_BitRate.DataContext =
tbxAudio_Volume.DataContext =
AudioViewModel.vm;
}
XAML ComboBox
<ComboBox x:Name="cboVideo_Quality"
DataContext="{Binding VideoViewModel}"
ItemsSource="{Binding Video_Quality_Items}"
SelectedItem="{Binding Video_Quality_SelectedItem}"
IsEnabled="{Binding Video_Quality_IsEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="105"
Height="22"
Margin="0,0,0,0"/>
Video ViewModel Class
public class VideoViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void OnPropertyChanged(string prop)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(prop));
}
}
public VideoViewModel() { }
public static VideoViewModel _vm = new VideoViewModel();
public static VideoViewModel vm
{
get { return _vm; }
set
{
_vm = value;
}
}
// Items Source
private List<string> _Video_Quality_Items = new List<string>()
{
"High",
"Medium",
"Low",
};
public List<string> Video_Quality_Items
{
get { return _Video_Quality_Items; }
set
{
_Video_Quality_Items = value;
OnPropertyChanged("Video_Quality_Items");
}
}
// Selected Item
private string _Video_Quality_SelectedItem { get; set; }
public string Video_Quality_SelectedItem
{
get { return _Video_Quality_SelectedItem; }
set
{
if (_Video_Quality_SelectedItem == value)
{
return;
}
_Video_Quality_SelectedItem = value;
OnPropertyChanged("Video_Quality_SelectedItem");
}
}
// Enabled
private bool _Video_Quality_IsEnabled;
public bool Video_Quality_IsEnabled
{
get { return _Video_Quality_IsEnabled; }
set
{
if (_Video_Quality_IsEnabled == value)
{
return;
}
_Video_Quality_IsEnabled = value;
OnPropertyChanged("Video_Quality_IsEnabled");
}
}
}
You can instantiate an object in xaml:
<Window.DataContext>
<local:MainWindowViewmodel/>
</Window.DataContext>
And you could do that for your usercontrol viewmodels as well.
It's more usual for any child viewmodels to be instantiated in the window viewmodel. Exposed as public properties and the datacontext of a child viewmodel then bound to that property.
I suggest you google viewmodel first and take a look at some samples.
I'm not sure if this is the correct way, but I was able to bind groups of ComboBoxes to different ViewModels.
I created one ViewModel to reference them all.
public class VM: INotifyPropertyChanged
{
...
public static MainViewModel MainView { get; set; } = new MainViewModel ();
public static VideoViewModel VideoView { get; set; } = new VideoViewModel ();
public static AudioViewModel AudioView { get; set; } = new AudioViewModel ();
}
I used Andy's suggestion <local:VM> in MainWindow.xaml.
<Window x:Class="MyProgram.MainWindow"
...
xmlns:local="clr-namespace:MyProgram"
>
<Window.DataContext>
<local:VM/>
</Window.DataContext>
And used a UserControl with DataContext set to VideoView, with ComboBoxes inside.
Instead of a UserControl, can also just use VideoView.Your_Property_Name on each binding.
<UserControl DataContext="{Binding VideoView}">
<StackPanel>
<ComboBox x:Name="cboVideo_Quality"
ItemsSource="{Binding Video_Quality_Items}"
SelectedItem="{Binding Video_Quality_SelectedItem}"
IsEnabled="{Binding Video_Quality_IsEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="105"
Height="22"
Margin="0,0,0,0"/>
<!-- Other ComboBoxes with DataContext VideoView in here -->
</StackPanel>
</UserControl>
Then to access one of the properties:
VM.VideoView.Video_Codec_SelectedItem = "x264";
VM.VideoView.Video_Quality_SelectedItem = "High";
VM.AudioView.Audio_Codec_SelectedItem = "AAC";
VM.AudioView.Audio_Quality_SelectedItem = "320k";
Others have obviously provided good answers, however, the underlying miss of your binding is your first set of DataContext = = = = = to the main view model.
Once you the main form's data context to the MAIN view model, every control there-under is expecting ITS STARTING point as the MAIN view model. Since the MAIN view model does not have a public property UNDER IT of the video and audio view models, it cant find them to bind do.
If you remove the "this.DataContext =", then there would be no default data context and each control SHOULD be able to be bound as you intended them.
So change
this.DataContext =
tbxInput.DataContext =
tbxOutput.DataContext =
cboPreset.DataContext =
MainViewModel.vm;
to
tbxInput.DataContext =
tbxOutput.DataContext =
cboPreset.DataContext =
MainViewModel.vm;
I'm creating a dynamic UI application in WPF MVVM that binds areas of the UI to a collection of my custom UIField class.
UIField contains a number of BindingProperty properties that allow me the specify the property the Control will bind to.
public abstract class UIField : BaseObject
{
BindingProperty property, isReadonly = new BindingProperty(false, false);
object dataContext;
public BindingProperty Property
{
get { return property; }
set { SetProperty(ref property, value, () => Property); }
}
public BindingProperty IsReadonly
{
get { return isReadonly; }
set { SetProperty(ref isReadonly, value, () => IsReadonly); }
}
public object DataContext
{
get { return dataContext; }
set { SetProperty(ref dataContext, value, () => DataContext); }
}
}
In a ResourceDictionary I template the derived classes of UIField
<DataTemplate DataType="{x:Type fields:TextBoxField}">
<TextBox metro:TextBoxHelper.Watermark="{e:IndirectBinding Watermark}"
metro:TextBoxHelper.UseFloatingWatermark="{e:IndirectBinding UseFloatingWatermark}"
IsReadOnly="{e:IndirectBinding IsReadonly}"
Text="{e:IndirectBinding Property}"
/>
</DataTemplate>
And I then create these fields in the view model and add them to a collection. The UI then binds to the collection using an ItemsControl
new TextBoxField()
{
DataContext = this,
Property = new BindingProperty(() => ((SalesOrder)Order).SecondSalesReference),
Watermark = new BindingProperty("Customer Ref. Number", false),
UseFloatingWatermark = new BindingProperty(true, false)
}
This is working great for normal fields but I'm running into a problem with DataGrid.
Preferably, I would like to be able to bind a collection of UIFields that have been extended to provide a Header property to the columns of a DataGrid and then template these to DataGridColumns but you can't have a DataGridColumn in a DataTemplate.
Currently, I've created an interface that gives me the header, DataGridColumn type and the contents that should be in each cell:
public interface IDataGridField
{
string Header { get; set; }
Type ColumnType { get; }
UIField CellContents { get; }
}
I then bind these to a DataGrids columns using:
public class DataGridHelper
{
public static readonly DependencyProperty BindableColumnsProperty = DependencyProperty.RegisterAttached("BindableColumns", typeof(ThreadSafeCollection<IDataGridField>), typeof(DataGridHelper),new UIPropertyMetadata(null, BindableColumnsPropertyChanged));
static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
var dataGrid = source as DataGrid;
var columns = e.NewValue as ThreadSafeCollection<IDataGridField>;
dataGrid.Columns.Clear();
if (columns == null)
return;
foreach (var column in columns)
{
var newColumn = Utility.CreateInstance(column.ColumnType) as DataGridColumn;
newColumn.Header = column.Header;
dataGrid.Columns.Add(newColumn);
}
}
public static void SetBindableColumns(DependencyObject element, ThreadSafeCollection<IDataGridField> value)
{
element.SetValue(BindableColumnsProperty, value);
}
public static ThreadSafeCollection<IDataGridField> GetBindableColumns(DependencyObject element)
{
return (ThreadSafeCollection<IDataGridField>)element.GetValue(BindableColumnsProperty);
}
}
This will give me columns with headers but no content in the DataGridCells.
How can I set the contents of each cell in the DataGridColumn created?
EDIT 1 (Progress?):
I realised that the DataGridColumn generated has to be a DataGridTemplateColumn so this opens up a few more properties.
I've modified IDataGridField, changing the UIField property to a Type instead.
Then, when generating the new DataGridColumn I just get the DataTemplate assigned to the selected Type
foreach (var column in columns)
{
var newColumn = new DataGridTemplateColumn();
var key = new DataTemplateKey(column.CellContentType);
var template = Application.Current.FindResource(key) as DataTemplate;
newColumn.CellTemplate = template;
newColumn.Header = column.Header;
dataGrid.Columns.Add(newColumn);
}
I think the only problem now is applying the bindings.
It seems you want to reinvent the wheel. The WPF DataGrid handle basic DataTypes like string, bool, int already quite well. But you will have your reasons why.
I think if you want to go for dynamic approach you want to set the CellTemplate or/and CellEditingTemplate instead of creating the "correct" column type. This would also work for more complex datatypes.
This is just XAML but it should show the direction you could go:
<DataGrid ItemsSource="{Binding}">
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox metro:TextBoxHelper.Watermark="{e:IndirectBinding Watermark}"
metro:TextBoxHelper.UseFloatingWatermark="{e:IndirectBinding UseFloatingWatermark}"
IsReadOnly="{e:IndirectBinding IsReadonly}"
Text="{e:IndirectBinding Property}"
/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
You could also use the AutoGeneratingColumn event of the datagrid to override the creation of columns. Maybe you want to derive your CustomDataGrid?
private void DataGrid_AutoGeneratingColumn(object sender, System.Windows.Controls.DataGridAutoGeneratingColumnEventArgs e)
{
//check for the type
if (e.PropertyType == typeof(TextField))
{
DataGridTemplateColumn newDataGridTemplateColumn = new DataGridTemplateColumn();
e.Column = newDataGridTemplateColumn;
//do some datatemplate voodoo, maybe you want to load it from resources or similar
StringReader stringReader = new StringReader(
#"<DataTemplate
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">
<" + typeof(ComboBox).Name + #" Text=""{Binding " + "Text" + #"}""/>
</DataTemplate>");
XmlReader xmlReader = XmlReader.Create(stringReader);
DataTemplate template = XamlReader.Load(xmlReader) as DataTemplate;
newDataGridTemplateColumn.CellTemplate = template;
}
}
I have a TextBlock in a ComboBox in a C# WPF project bound to a list of 'Envelope' items, which have a string 'Name' and a double 'Weight' property, the former of which I would like to see displayed in the TextBlock.
When I run my program, the ComboBox appears without any text in it. It properly has three unlabeled items in it, and if I view the ItemsSource or SelectedItem of the ComboBox they show the appropriate values, and other code which interacts with the SelectedItem of the ComboBox behaves properly. The only thing that does not work is that the TextBlock contains no text. If I replace the "{Binding Name}" with "au ghdfjlnvgmumar" then the appropriate garbled characters appear in the ComboBox, so it is definitely a problem with the binding. What is the problem, and how can I get around it?
Relevant code:
xaml:
<ComboBox Name="EnvelopeList" HorizontalAlignment="Center" Width="200" >
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
C#:
//main window code
public MainWindow()
{
InitializeComponent();
envelopes = new List<Envelope>();
envelopes.Add(new Envelope("TEST", 0));
envelopes.Add(new Envelope("HI", 10));
EnvelopeList.ItemsSource = envelopes;
}
//Envelope class
class Envelope
{
public string Name;
public double Weight;
public Envelope()
{
Name = "[None]";
Weight = 0;
}
public Envelope(string n, double w)
{
Name = n;
Weight = w;
}
public override string ToString()
{
return Name;
}
}
When DataBinding, you can only bind to Properties. Also, you need to update your properties with a PropertyChangedEvent. Otherwise, if you change your property after the initial binding it won't update the UI.
You need to use on property changed and a property
public class Envelope: ModelBase
{
private string _name;
public string Name
{
get { return _name; }
set { _name= value; OnPropertyChanged("Name"); }
}
}
public class ModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propName));
}
}
}
Finally, I notice that you're setting ItemsSource directly. Instead, you want to set your View's DataContext property and then bind to your ItemsSource
Here is a MSDN article on DataBinding that will teach you how to do it properly.
Name is a field, you can only bind to properties.
In two combobox A and B.
A's ItemsSource is Custom list. and B's ItemsSource is UserControl list.
When manually setting the SelectedItem, A combobox works well, but B combobox UI do not show the selected Item. (In debugging, SelectedItem's value mapping is right, but the combobox B's UI do not be changed.)
All the other structure is same between A and B. What is the reason?
MainWindow.xaml
...
<ComboBox ItemsSource="{Binding FruitList}" SelectedItem="{Binding SelectedFruit}"
DisplayMemberPath="FruitName" />
<Button Content="Button" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="75" Click="Button_Click"/>
<ComboBox ItemsSource="{Binding UserControlList}" SelectedItem="{Binding SelectedUserControl}" DisplayMemberPath="ItemName" />
<Button Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Click="Button_Click2"/>
</Grid>
MainWindow.xaml.cs
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
FruitList.Add(f1);
FruitList.Add(f2);
FruitList.Add(f3);
UserControlList.Add(u1);
UserControlList.Add(u2);
UserControlList.Add(u3);
}
Fruit f1 = new Fruit { FruitName = "Apple" };
Fruit f2 = new Fruit { FruitName = "Banana" };
Fruit f3 = new Fruit { FruitName = "Lemon" };
MyUserControl u1 = new MyUserControl { ItemName = "Apple" };
MyUserControl u2 = new MyUserControl { ItemName = "Banana" };
MyUserControl u3 = new MyUserControl { ItemName = "Lemon" };
ObservableCollection<Fruit> _FruitList = new ObservableCollection<Fruit>();
public ObservableCollection<Fruit> FruitList
{
get { return _FruitList; }
set
{
_FruitList = value;
OnPropertyChanged();
}
}
Fruit _SelectedFruit;
public Fruit SelectedFruit
{
get { return _SelectedFruit; }
set
{
_SelectedFruit = value;
OnPropertyChanged();
}
}
ObservableCollection<MyUserControl> _UserControlList = new ObservableCollection<MyUserControl>();
public ObservableCollection<MyUserControl> UserControlList
{
get
{
return _UserControlList;
}
set
{
_UserControlList = value;
OnPropertyChanged();
}
}
MyUserControl _SelectedUserControl;
public MyUserControl SelectedUserControl
{
get { return _SelectedUserControl; }
set
{
_SelectedUserControl = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string caller = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.SelectedFruit = f3;
}
private void Button_Click2(object sender, RoutedEventArgs e)
{
this.SelectedUserControl = u3;
}
}
public class Fruit
{
public string FruitName { get; set; }
}
}
UserControl
public partial class MyUserControl : UserControl
{
public MyUserControl()
{
InitializeComponent();
}
public string ItemName { get; set; }
}
This is not the good way of achieving this. Better define the ItemTemplate for the combobox to have the UserControl in it like:
<ComboBox ItemsSource="{Binding ItemList}" SelectedItem="{Binding SelectedItem}" >
<ComboBox.ItemTemplate>
<DataTemplate>
<myControls:MyUserControl/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
and define the class Item
public class Item
{
public string ItemName { get; set; }
}
ObservableCollection<Item> _ItemsList = new ObservableCollection<Item>();
public ObservableCollection<Item> ItemsList
{
get
{
return _ItemsList ;
}
set
{
_ItemsList = value;
OnPropertyChanged();
}
}
Here DataContext of your UserControl will be Item object. you can bind the ItemName within you user control to show it in anyway you want.
in your user control you can have:
<TextBlock Text="{Binding ItemName}"></TextBlock>
Since you have asked "What is the reason?":
The reason why the second combo box does not show any selection is that ComboBox handles items of type ContentControl specially. In the read-only selection box, it is not the ContentControl that is used to display the value, but the content of the ContentControl. Since a UserControl is a ContentControl, the content of the UserControl is displayed inside the selection box, and therefore you have lost the data context of the UserControl; in the end, an empty string is displayed even though SelectedItem contains a reference to the UserControl that still has a valid data context. (As far as I know this behavior is undocumented; but you can see that it works like this by examining the ComboBox's code on http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/ComboBox.cs, especially the UpdateSelectionBoxItem() method).
By setting IsEditable="True" on the second ComboBox, you can see that everything works fine if the combo box has no read-only selection box.
Therefore, you generally should avoid adding UI elements to combo boxes, especially if you are using the DisplayMemberPath property, i.e. if you never want to actually display the UI element.
The recommended way to display ComboBox items with non-standard appearance (e.g. with UserControls) is described in the answer of #nit.
If you, however, insist on passing a UserControl item list to the ComboBox, you might remove DisplayMemberPath and use something like this:
<ComboBox ItemsSource="{Binding UserControlList}" SelectedItem="{Binding SelectedUserControl}" >
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ItemName}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Furthermore, in the constructor of your UserControl, you must place this line:
((FrameworkElement) Content).DataContext = this;
This is necessary to make sure that the correct data context is available in the read-only selection box, which only contains the content of the user control, not the user control itself.
Please note, that with the above example, the drop-down list contains text only (i.e. the item names), but the selection box will contain the fully rendered user control.