A collection of StackPanel as ItemsSource of ComboBox - wpf

I have
a collection of StackPanel which each one includes a dynamic set of controls (based on database values), I want to set them as ItemsSource of some ComboBox
for example i have two database values which should be generated:
In DB i have these:
row 1=>Class [a] p [B] , [AB]vb
row 2=>Class tpy rbs=[sdfg],kssc[h] hm
and each one should generate as a ComboBox column like the fallowing:
In ComboBox I wanna generate these :
ComboBoxItem 1 :Class [a textBox] p [a textBox] , [a textBox]vb
ComboBoxItem 2 :Class tpy rbs=[a textBox].kssc[a textBox] hm
the fallowing code is doing this right:
Class ConvertToControlsFormat()
{
Regex exp = new Regex(#"\[\w*\]");
var source = new TestEntities().cmbSources;
foreach (var item in source)
{
StackPanel p = new StackPanel { Orientation = Orientation.Horizontal, FlowDirection = FlowDirection.LeftToRight };
int i = 0;
foreach (string txt in exp.Split(item.Title))
{
p.Children.Add(new TextBlock { Text = txt });
if (i < exp.Matches(item.Title).Count)
p.Children.Add(new TextBox { Text = exp.Matches(item.Title)[i].Value, Width = 30 });
}
cmb.Items.Add(p);
}
}
But I cant set TwoWay DataBindings for that, so I created a list of StackPanel as a field of cmbSource class (which is bound to ItemsSource of the ComboBox)
public partial class cmbSource
{
#region Primitive Properties
int iD;
public virtual int ID
{
get
{
if (Title != null)
ControlsCollection = SetControlsCollection(Title);
return iD;
}
set
{
iD = value;
}
}
private StackPanel SetControlsCollection(string ttl)
{
Regex exp = new Regex(#"\[\w*\]");
StackPanel p = new StackPanel { Orientation = Orientation.Horizontal, FlowDirection = System.Windows.FlowDirection.LeftToRight };
int i = 0;
foreach (string txt in exp.Split(ttl))
{
p.Children.Add(new TextBlock { Text = txt });
if (i < exp.Matches(ttl).Count)
p.Children.Add(new TextBox { Text = exp.Matches(ttl)[i].Value, Width = 30 });
}
return p;
}
public virtual string Title
{
get;
set;
}
public virtual StackPanel ControlsCollection
{
get;
set;
}
#endregion
}
but I have no idea of how bind it to ItemsSource of my ComboBox
Summery:I want to bind a list of controls to a ComboBox
any suggestions!? thank you.

EDIT
First: you do not bind a ComboBox to a collection of UI Elements. That is not the way WPF works. Container controls such as the Grid, StackPanel and Canvas can contain child controls. ItemsControls such as the ComboBox contain data objects and use DataTemplates to display the items.
Secondly: if the database can contain ANY data that could cause ANY UI to be needed you will need to generate the UI in code by creating StackPanels etc. adding controls and bindings as you do in your code examples.
Thirdly: the reason you can't bind is that the data from the database is a string that you split into parts; there is no way you can simply go back to the string.
Suggestion: the string in the database is probably (I hope) in some sort of format. Using that knowledge you could generate a new format string when you are parsing the database string. E.g., when the database contains foo [bar] you could generate {0} [bar]. On a save action from the user you could use that string to create the updated string for the database by using: String.Format("{0} [bar]", someControl.Text)
Extra: Please, next time, use better names and example texts; the question is unreadable like this. There is no way you can expect us to understand 2=>Class tpy rbs=[sdfg],kssc[h] hm
OLD ANSWER
Make a class Stuff, implementing INotifyPropertyChanged and having the properties Name and Value.
Load the database data into an ObservableCollection<Stuff> and bind the ComboBox to this collection.
Set the ItemTemplate of the combo box to a datatemplate like this:
<ComboBox ItemsSource="{Binding}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
<TextBox Text="{Binding Value}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

Related

How to set combobox selectedvalue

Need help on below errors highlighted in the code comments shown below
The purpose of the below method is to find an combox item byvalue and set it to combobox if present else add it to combobox and then set it.
private void SetComboBoxValueHelper(ComboBox cb, string valuetoSet)
{
bool isValueNotFound = false;
cb.SelectedValue = valuetoSet;
isValueNotFound = string.IsNullOrEmpty(Convert.ToString(cb.SelectedValue));
if (isValueNotFound)
{
//try to ignore case and find the item in combobox
foreach (ComboBoxItem item in cb.Items) //1.ERROR AFTER ANY ITEM ADDED using my code
{
if (string.Compare(Convert.ToString(item.Content), valuetoSet, true) == 0)
{
cb.SelectedValue = item.Content;
isValueNotFound = false;
}
}
//if still not found add the item to the combobox
if (isValueNotFound)
{
cb.Items.Add(valuetoSet);
cb.SelectedValue = valuetoSet;//2.THIS IS NOT WORKING
}
}
}
A sample combobox i use is
<ComboBox Grid.Column="5" Grid.Row="4" Margin="10" Name="cbbox1" SelectedValuePath="Content">
<ComboBoxItem Content="No" IsSelected="True" />
<ComboBoxItem Content="Yes" />
</ComboBox>
Please let me know
a)how i can fix that non working line.
b)I get error at line shown in comment. how do i prevent it.
The problem here is that you are adding a string to the ComboBox's Items:
cb.Items.Add(valuetoSet);
You should instead add a new ComboBoxItem:
cb.Items.Add(new ComboBoxItem { Content = valuetoSet });
Otherwise you mix ComboBoxItems and strings in the Items collection. Now when you iterate over the items as ComboBoxItems, you get an exception when the added string item is encountered.
You should however consider to use string items instead of ComboBoxItems. That would make your code cleaner and you could address the selected string item directly by the SelectedItem property, without any need for SelectedValuePath and stuff like Convert.ToString(item.Content).
You could even define the initial item strings in XAML like this:
<ComboBox xmlns:sys="clr-namespace:System;assembly=mscorlib"
SelectedIndex="0" ...>
<sys:String>No</sys:String>
<sys:String>Yes</sys:String>
</ComboBox>
Now your whole SetComboBoxValueHelper method would simplify as Novitchi has written:
private void SetComboBoxValueHelper(ComboBox cb, string valuetoSet)
{
if (!cb.Items.Contains(valuetoSet))
{
cb.Items.Add(valuetoSet);
}
cb.SelectedItem = valuetoSet;
}
EDIT: If there is still any need to iterate over the items, you would also iterate over strings instead of ComboBoxItems:
foreach (string item in cb.Items)
{
...
}
As Clemens already suggested you shouldn't mix the ComboBoxItems and String. From XAML you adding to combobox ComboBoxItems, from code you adding Strings. A simple solution will be to set all Items as string. For this you should add your Yes, No items from code too.
Then your SetComboBoxValueHelper should look like this:
private void SetComboBoxValueHelper(ComboBox cb, string valuetoSet)
{
bool valueNotFound = !cb.Items.Contains(valuetoSet);
if (valueNotFound)
cb.Items.Add(valuetoSet);
cb.SelectedItem = valuetoSet;
}
The wpf will create the ComboBoxItem for you and you can get it using
cb.ItemContainerGenerator.ContainerFromItem("ItemString");

WPF: What is more easy is convinient to develop dynamically?

I have a DataGrid (dataGrid1) where records can be added and deleted.
Based on that dataGrid1, I want to make a new Grid with buttons in it based on ID and Types'. Cols will also have to given a DataSource of add dynamically, but that will be just while generating for the 1st time in Window_Loaded itself. Rows can be added/removed based on changes in dataGrid1. I want somethign like this :
On each Btn click, a new window will be opened for entry of the particular Type and for the particular ID. If the details are already entered, then the text of btn wil be "Update" else "Add".
What could be the best resource/control to perform this operations ? At present, I just did a Grid with 2 stable cols. Any ideas for the above to use Grid, DataGrid or something else. And adding/removing rows will be easy in which way and how.
Any help is appreciated.
Okay, let me try to take an example which is similar to your needs
Let's assume we use this class:
public class MyObject
{
public int MyID;
public string MyString;
public ICommand MyCommand;
}
And we are willing to display a DataGrid listing the ID, and having as a second column a Button, with the property MyString as content, which, when clicked, launches the ICommand MyCommand which opens in a new window whatever you want.
Here is what you should have on the View side:
<DataGrid ItemsSource="{Binding MyList}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding MyID}" />
<DataGridTemplateColumn Header="Buttons">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="{Binding MyString}" Command="{Binding MyCommand}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
This will show a DataGrid taking all the content in an IEnumerable<MyObject> named 'MyList', and shows two columns as defined before.
Now if you need to define the command.
First, I recommend you read this introductory link to MVVM and take the RelayCommand class (that's what we're gonna use for your problem)
So, in your ViewModel, the one which defines the MyList, here is how you should define some of the useful objects:
public ObservableCollection<MyObject> MyList { get; set; }
// blah blah blah
public void InitializeMyList()
{
MyList = new ObservableCollection<MyObject>();
for (int i = 0; i < 5; i++)
{
MyList.Add(InitializeMyObject(i));
}
}
public MyObject InitializeMyObject(int i)
{
MyObject theObject = new MyObject();
theObject.MyID = i;
theObject.MyString = "The object " + i;
theObject.MyCommand = new RelayCommand(param =< this.ShowWindow(i));
return theObject
}
private void ShowWindow(int i)
{
// Just as an exammple, here I just show a MessageBox
MessageBox.Show("You clicked on object " + i + "!!!");
}
This should be enough to create whatever you want. As you can see, every Button will call a method (ShowWindow) which is defined to show your new window, do whatever you need inside. The RelayCommand is actually just here, as its name says, to relay the command fired by the button to a method which contains the execution logic.
And... I think that's all you need. Sorry for the late answer BTW
EDIT - generating columns manually/dynamically
The following code is part of a code I had to do when I had a similar problem.
My problem was, I needed to change the columns displayed every time a ComboBox's SelectedItem would change. So I put this in a SelectionChanged event handler.
I don't know where exactly do you need to generate your columns, but I'll give you a general example.
Assume your ItemsSource is an ObservableCollection<MyNewObject>
MyNewObject is the following:
public class MyNewObject
{
public IList<string> MyStrings { get; set; }
}
You should put somewhere in your code (should be when you need to generate the column) the following code, which is generating a number of columns equal to the length of the first MyNewObject from the list (note: this is in code-behind, and the DataGrid you're working on is named dataGrid)
ObservableCollection<MyNewObject> source = dataGrid.ItemsSource as ObservableCollection<MyNewObject>;
if (source == null || source.Count == 0)
{
return;
}
MyNewObject firstObject = source[0];
for(int i = 0; i < firstObject.MyStrings.Count; i++)
{
// Creates one column filled with buttons for each string
DataGridTemplateColumn columnToAdd = new DataGridTemplateColumn();
columnToAdd.Width = 110; // I set a manual width, but you can do whatever you want
columnToAdd.Header = "Header number " + i;
// Create the template with a Button inside, bound to the appropriate string
DataTemplate dataTemplate = new DataTemplate(typeof(Button));
FrameworkElementFactory buttonElement = new FrameworkElementFactory(typeof(Button));
Binding binding = new Binding("MyStrings[" + i + "]");
binding.Mode = BindingMode.TwoWay;
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
buttonElement.SetBinding(Button.ContentProperty, binding);
// Do the same here for your command, or for whatever you want to do when the user clicks on this button
dataTemplate.VisualTree = buttonElement;
columnToAdd.CellTemplate = dataTemplate;
dataGrid.Columns.Add(columnToAdd);
}
This will create one column for each string found in the first object. Then, enhance it with whatever command or display trick you need!

wpf toolkit, datagrid, comboboxcolumn

In a datagrid I have two DataGridComboBoxColumns. The items of one of these columns should depend on what is selected in the other column. The underlying collection used to model this is a dictionary<string,List<string>>. How should i go about implementing this? I can't seem to hook up to any relevant events on the columns, and I cant find any databinding scenarios that support this..
I had the same scenario a while back and fixed it like this:
public class DataItem : INotifyPropertyChanged {
...
public List<SomeObject> DisplayableComboBoxItems {
get; set;
}
private static Dictionary<int, List<SomeObject>> myDict;
public Dictionary<int, List<SomeObject>> MyDict {
get {
if (myDict == null) {
myDict = GetYourDataFromSomewhere();
}
return myDict;
}
}
public int TypeId {
get { return typeId; }
set {
if (value == typeId) return;
typeId = value;
RaisePropertyChanged("TypeId");
}
}
public int TypeSetId {
get { return typeSetId; }
set {
if (typeSetId == value) return;
typeSetId = value;
RaisePropertyChanged("TypeSetId");
DisplayableComboBoxItems = MyDict[typeSetId];
RaisePropertyChanged("DisplayableComboBoxItems");
TypeId = 0;
}
}
...
}
DataItem is the object that gets bound to a DataRow.
This is just a small mock-up of the code. Basically, whenever the TypeSet changes, I needed a new list of Types to be displayed. I used just a static list, in this example i used a dictionary.
With this setup you can bind you combobox ItemsSource to the 'DisplayableComboBoxItems', and your SelectedValue to "TypeId".
You're gonna need other properties to display the correct text instead of the TypeId.
The downside of this is that when you have 1000+ items, you'll have that same list for all items. This wasn't however the case with me (DataGrid showed max 50 items).
I hope this is clear enough and that it helps you in the right direction!
cheers!
Roel
Instead of using a DataGridComboBoxColumn for the second column, I went with a DataGridTemplateColumn with an embedded Combobox. For the itemsource i defined a converter: string -> List<string>. The converter translates the value of the selecteditem of the other DataGridComboBox (which is bound to Navn) into List<string>, this is just a dictionary lookup.
Like so:
<my:DataGridTemplateColumn>
<my:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox SelectedItem="{Binding Værdi}"
ItemsSource="{Binding Navn, Converter={StaticResource dimensionToValues}}"
>
</ComboBox>
</DataTemplate>
</my:DataGridTemplateColumn.CellTemplate>
</my:DataGridTemplateColumn>

WPF ComboBox binding behaviour

I have the following XAML markup:
<TextBox x:Name="MyTextBox" Text="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Name="MyComboBox" ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"
SelectedValue="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber}"
SelectedValuePath="ProductNumber" />
My View's DataContext is bound to a viewmodel containing a public property called SelectedCustomer. Customer objects contain a FavouriteProduct property of type Product and Product objects contain public properties ProductNumber and ProductName.
The behaviour I'm looking for is to have the SelectedItem of the ComboBox update the Text in the TextBox and vice versa. ComboBox to TextBox works just fine. Selecting any product in the ComboBox updates the TextBox with the product number of that product. However when I try to go the other way I get som strange behaviour. It only works for the items that come before the selected item. I will try to explain:
Consider the following list of products ([Product Number], [Product Name]):
Fanta
Pepsi
Coca Cola
Sprite
Water
Now lets say that the SelectedCustomer's favourite product is Coca Cola (must be a developer). So when the window opens the TextBox reads 3 and the ComboBox reads Coca Cola. Lovely. Now lets change the product number in the TextBox to 2. The ComboBox updates it's value to Pepsi. Now try to change the product number in the TextBox to anything higher then the number for Coca Cola (3). Not so lovely. Selecting either 4 (Sprite) or 5 (Water) makes the ComboBox revert back to Coca Cola. So the behaviour seems to be that anything below the item that you open the window width from the list in the ItemSource does not work. Set it to 1 (Fanta) and none of the others work. Set it to 5 (Water) and they all work. Could this have to do with some initialisation for the ComboBox? Potential bug? Curious if anyone else have seen this behaviour.
UPDATE:
After reading Mike Brown's response I have created properties for SelectedProduct and SelectedProductNumber. The problem I am having with this is that as soon as you select something from the ComboBox you end up in an endless loop where the properties keep updatign each other. Have I implemented the OnPropertyChanged handler incorrectly or is there something I am missing? Here is a snippet of code from my ViewModel:
private int _SelectedProductNumber = -1;
public int SelectedProductNumber
{
get
{
if (_SelectedProductNumber == -1 && SelectedCustomer.Product != null)
_SelectedProductNumber = SelectedCustomer.Product.ProductNumber;
return _SelectedProductNumber;
}
set
{
_SelectedProductNumber = value;
OnPropertyChanged("SelectedProductNumber");
_SelectedProduct = ProductList.FirstOrDefault(s => s.ProductNumber == value);
}
}
private Product _SelectedProduct;
public Product SelectedProduct
{
get
{
if (_SelectedProduct == null)
_SelectedProduct = SelectedCustomer.Product;
return _SelectedProduct;
}
set
{
_SelectedProduct = value;
OnPropertyChanged("SelectedProduct");
_SelectedProductNumber = value.ProductNumber;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
UPDATE 2
I have changed the implementation slightly now by updating the SelectedCustomer.FavouriteProduct from both properties and then using that when reading their values. This now works but I'm not sure it's the 'correct way'.
private int _SelectedProductNumber = 0;
public int SelectedProductNumber
{
get
{
if (SelectedCustomer.Product != null)
_SelectedProductNumber = SelectedCustomer.Product.ProductNumber;
return _SelectedProductNumber;
}
set
{
_SelectedProductNumber = value;
SelectedCustomer.FavouriteProduct = ProductList.FirstOrDefault(s => s.ProductNumber == value);
OnPropertyChanged("SelectedProductNumber");
OnPropertyChanged("SelectedProduct");
}
}
private Product _SelectedProduct;
public Product SelectedProduct
{
get
{
if (SelectedCustomer.Product != null)
_SelectedProduct = SelectedCustomer.Product;
return _SelectedProduct;
}
set
{
_SelectedProduct = value;
SelectedCustomer.FavouriteProduct = value;
OnPropertyChanged("SelectedProduct");
OnPropertyChanged("SelectedProductNumber");
}
}
Your aim is not too clear so I have written the folloiwng so support either options I can see.
To keep two elements bound to one item in sync you can set the IsSynchronizedWithCurrentItem="True" on your combobox as shown below:
<TextBox x:Name="MyTextBox" Text="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Name="MyComboBox" ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"
SelectedValue="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber}"
IsSynchronizedWithCurrentItem="True"
SelectedValuePath="ProductNumber" />
This will mean everything in the current window bound to the same background object will keep in sync and not give the odd behaviours you are seeing.
This quote form this longer MSDN article describes the effect:
The IsSynchronizedWithCurrentItem
attribute is important in that, when
the selection changes, that is what
changes the "current item" as far as
the window is concerned. This tells
the WPF engine that this object is
going to be used to change the current
item. Without this attribute, the
current item in the DataContext won't
change, and therefore your text boxes
will assume that it is still on the
first item in the list.
Then setting the Mode=TwoWay as suggested by the other answer will only ensure that both when you update the textbox the underlying object will be updated and when you update the object the textbox is updated.
This makes the textbox edit the selected items text and not select the item in the combolist with the matching text (which is the alternative think you are may be trying to achieve?)
To achieve the synchronised selection effect it may be worth setting IsEditable="True" on the combobox to allow users to type items in and dropping the text box. Alternatively if you need two boxes replace the textbox with a second combobox with IsSynchronizedWithCurrentItem="True" and IsEditable="True" then a styled to make it like a text box.
What you want to do is expose separate properties on your ViewModel for the currently selected product and currently selected product number. When the selected product is changed, update the product number and vice versa. So your viewmodel should look something like this
public class MyViewModel:INotifyPropertyChanged
{
private Product _SelectedProduct;
public Product SelectedProduct
{
get { return _SelectedProduct; }
set
{
_SelectedProduct = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedProduct"));
_SelectedProductID = _SelectedProduct.ID;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedProductID"));
}
}
private int _SelectedProductID;
public int SelectedProductID
{
get { return _SelectedProductID; }
set
{
_SelectedProductID = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedProductID"));
_SelectedProduct = _AvailableProducts.FirstOrDefault(p => p.ID == value);
PropertyChanged(this,new PropertyChangedEventArgs("SelectedProduct"));
}
}
private IEnumerable<Product> _AvailableProducts = GetAvailableProducts();
private static IEnumerable<Product> GetAvailableProducts()
{
return new List<Product>
{
new Product{ID=1, ProductName = "Coke"},
new Product{ID = 2, ProductName="Sprite"},
new Product{ID = 3, ProductName = "Vault"},
new Product{ID=4, ProductName = "Barq's"}
};
}
public IEnumerable<Product> AvailableProducts
{
get { return _AvailableProducts; }
}
private Customer _SelectedCustomer;
public Customer SelectedCustomer
{
get { return _SelectedCustomer; }
set
{
_SelectedCustomer = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedCustomer"));
SelectedProduct = value.FavoriteProduct;
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
So now your XAML binds to the appropriate properties and the viewModel is responsible for syncrhronization
<TextBox
x:Name="MyTextBox"
Text="{Binding Path=SelectedProductID, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox
x:Name="MyComboBox"
ItemsSource="{Binding AvailableProducts}"
DisplayMemberPath="ProductName"
SelectedItem="{Binding SelectedProduct}" />
Don't forget to implement the rest of INotifyPropertyChanged and the GetAvailableProducts function. Also there may be some errors. I hand typed this here instead of using VS but you should get the general idea.
Try:
SelectedItem="{Binding Path=YourPath, Mode=TwoWay"}
instead of setting SelectedValue and SelectedValuePath.
Might work with SelectedValue too, don't forget the Mode=TwoWay, since this isn't the default.
A good approuch would to use the master detail pattern - bind the master (the items view, e.g. combobox) to the data source collection and the detail view (e.g. text box) to the selected item in the source collection, using a binding converter to read/write the appropriate property.
Here is an example:
http://blogs.microsoft.co.il/blogs/tomershamam/archive/2008/03/28/63397.aspx
Notice the master binding is of the form {Binding} or {Binding SourceCollection} and the details binding is of the form {Binding } or {Binding SourceCollection}.
To get this working you need to wrap you collection with an object that keeps the selected item. WPF has one of these built-in: ObjectDataProvider.
Example:
http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/068977c9-95a8-4b4a-9d38-b0cc36d06446

WPF GridView with a dynamic definition

I want to use the GridView mode of a ListView to display a set of data that my program will be receiving from an external source. The data will consist of two arrays, one of column names and one of strings values to populate the control.
I don't see how to create a suitable class that I can use as the Item in a ListView. The only way I know to populate the Items is to set it to a class with properties that represent the columns, but I have no knowledge of the columns before run-time.
I could create an ItemTemplate dynamically as described in: Create WPF ItemTemplate DYNAMICALLY at runtime but it still leaves me at a loss as to how to describe the actual data.
Any help gratefully received.
You can add GridViewColumns to the GridView dynamically given the first array using a method like this:
private void AddColumns(GridView gv, string[] columnNames)
{
for (int i = 0; i < columnNames.Length; i++)
{
gv.Columns.Add(new GridViewColumn
{
Header = columnNames[i],
DisplayMemberBinding = new Binding(String.Format("[{0}]", i))
});
}
}
I assume the second array containing the values will be of ROWS * COLUMNS length. In that case, your items can be string arrays of length COLUMNS. You can use Array.Copy or LINQ to split up the array. The principle is demonstrated here:
<Grid>
<Grid.Resources>
<x:Array x:Key="data" Type="{x:Type sys:String[]}">
<x:Array Type="{x:Type sys:String}">
<sys:String>a</sys:String>
<sys:String>b</sys:String>
<sys:String>c</sys:String>
</x:Array>
<x:Array Type="{x:Type sys:String}">
<sys:String>do</sys:String>
<sys:String>re</sys:String>
<sys:String>mi</sys:String>
</x:Array>
</x:Array>
</Grid.Resources>
<ListView ItemsSource="{StaticResource data}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Path=[0]}" Header="column1"/>
<GridViewColumn DisplayMemberBinding="{Binding Path=[1]}" Header="column2"/>
<GridViewColumn DisplayMemberBinding="{Binding Path=[2]}" Header="column3"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
Thanks, that is very helpful.
I used it to create a dynamic version as follows. I created the column headings as you suggested:
private void AddColumns(List<String> myColumns)
{
GridView viewLayout = new GridView();
for (int i = 0; i < myColumns.Count; i++)
{
viewLayout.Columns.Add(new GridViewColumn
{
Header = myColumns[i],
DisplayMemberBinding = new Binding(String.Format("[{0}]", i))
});
}
myListview.View = viewLayout;
}
Set up the ListView very simply in XAML:
<ListView Name="myListview" DockPanel.Dock="Left"/>
Created an wrapper class for ObservableCollection to hold my data:
public class MyCollection : ObservableCollection<List<String>>
{
public MyCollection()
: base()
{
}
}
And bound my ListView to it:
results = new MyCollection();
Binding binding = new Binding();
binding.Source = results;
myListview.SetBinding(ListView.ItemsSourceProperty, binding);
Then to populate it, it was just a case of clearing out any old data and adding the new:
results.Clear();
List<String> details = new List<string>();
for (int ii=0; ii < externalDataCollection.Length; ii++)
{
details.Add(externalDataCollection[ii]);
}
results.Add(details);
There are probably neater ways of doing it, but this is very useful for my application. Thanks again.
This article on the CodeProject explains exactly how to create a Dynamic ListView - when data is known only at runtime.
http://www.codeproject.com/KB/WPF/WPF_DynamicListView.aspx
Not sure if it's still relevant but I found a way to style the individual cells with a celltemplate selector. It's a bit hacky because you have to munch around with the Content of the ContentPresenter to get the proper DataContext for the cell (so you can bind to the actual cell item in the cell template):
public class DataMatrixCellTemplateSelectorWrapper : DataTemplateSelector
{
private readonly DataTemplateSelector _ActualSelector;
private readonly string _ColumnName;
private Dictionary<string, object> _OriginalRow;
public DataMatrixCellTemplateSelectorWrapper(DataTemplateSelector actualSelector, string columnName)
{
_ActualSelector = actualSelector;
_ColumnName = columnName;
}
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
// The item is basically the Content of the ContentPresenter.
// In the DataMatrix binding case that is the dictionary containing the cell objects.
// In order to be able to select a template based on the actual cell object and also
// be able to bind to that object within the template we need to set the DataContext
// of the template to the actual cell object. However after the template is selected
// the ContentPresenter will set the DataContext of the template to the presenters
// content.
// So in order to achieve what we want, we remember the original DataContext and then
// change the ContentPresenter content to the actual cell object.
// Therefor we need to remember the orginal DataContext otherwise in subsequent calls
// we would get the first cell object.
// remember old data context
if (item is Dictionary<string, object>)
{
_OriginalRow = item as Dictionary<string, object>;
}
if (_OriginalRow == null)
return null;
// get the actual cell object
var obj = _OriginalRow[_ColumnName];
// select the template based on the cell object
var template = _ActualSelector.SelectTemplate(obj, container);
// find the presenter and change the content to the cell object so that it will become
// the data context of the template
var presenter = WpfUtils.GetFirstParentForChild<ContentPresenter>(container);
if (presenter != null)
{
presenter.Content = obj;
}
return template;
}
}
Note: I changed the DataMatrix frome the CodeProject article so that rows are Dictionaries (ColumnName -> Cell Object).
I can't guarantee that this solution will not break something or will not break in future .Net release. It relies on the fact that the ContentPresenter sets the DataContext after it selected the template to it's own Content. (Reflector helps a lot in these cases :))
When creating the GridColumns, I do something like that:
var column = new GridViewColumn
{
Header = col.Name,
HeaderTemplate = gridView.ColumnHeaderTemplate
};
if (listView.CellTemplateSelector != null)
{
column.CellTemplateSelector = new DataMatrixCellTemplateSelectorWrapper(listView.CellTemplateSelector, col.Name);
}
else
{
column.DisplayMemberBinding = new Binding(string.Format("[{0}]", col.Name));
}
gridView.Columns.Add(column);
Note: I have extended ListView so that it has a CellTemplateSelector property you can bind to in xaml
#Edit 15/03/2011:
I wrote a little article which has a little demo project attached: http://codesilence.wordpress.com/2011/03/15/listview-with-dynamic-columns/
Totally progmatic version:
var view = grid.View as GridView;
view.Columns.Clear();
int count=0;
foreach (var column in ViewModel.GridData.Columns)
{
//Create Column
var nc = new GridViewColumn();
nc.Header = column.Field;
nc.Width = column.Width;
//Create template
nc.CellTemplate = new DataTemplate();
var factory = new FrameworkElementFactory(typeof(System.Windows.Controls.Border));
var tbf = new FrameworkElementFactory(typeof(System.Windows.Controls.TextBlock));
factory.AppendChild(tbf);
factory.SetValue(System.Windows.Controls.Border.BorderThicknessProperty, new Thickness(0,0,1,1));
factory.SetValue(System.Windows.Controls.Border.MarginProperty, new Thickness(-7,0,-7,0));
factory.SetValue(System.Windows.Controls.Border.BorderBrushProperty, Brushes.LightGray);
tbf.SetValue(System.Windows.Controls.TextBlock.MarginProperty, new Thickness(6,2,6,2));
tbf.SetValue(System.Windows.Controls.TextBlock.HorizontalAlignmentProperty, column.Alignment);
//Bind field
tbf.SetBinding(System.Windows.Controls.TextBlock.TextProperty, new Binding(){Converter = new GridCellConverter(), ConverterParameter=column.BindingField});
nc.CellTemplate.VisualTree = factory;
view.Columns.Add(nc);
count++;
}
I would go about doing this by adding an AttachedProperty to the GridView where my MVVM application can specify the columns (and perhaps some additional metadata.) The Behavior code can then dynamically work directly with the GridView object to create the columns. In this way you adhere to MVVM and the ViewModel can specify the columns on the fly.

Resources