Create WPF ItemTemplate DYNAMICALLY at runtime - wpf

At run time I want to dynamically build grid columns (or another display layout) in a WPF ListView. I do not know the number and names of the columns before hand.
I want to be able to do:
MyListView.ItemSource = MyDataset;
MyListView.CreateColumns();

You can add columns dynamically to a ListView by using Attached Properties. Check out this article on the CodeProject it explains exactly that...
WPF DynamicListView - Binding to a DataMatrix

From MSDN:
MyListBox.ItemsSource = view;
ListView myListView = new ListView();
GridView myGridView = new GridView();
myGridView.AllowsColumnReorder = true;
myGridView.ColumnHeaderToolTip = "Employee Information";
GridViewColumn gvc1 = new GridViewColumn();
gvc1.DisplayMemberBinding = new Binding("FirstName");
gvc1.Header = "FirstName";
gvc1.Width = 100;
myGridView.Columns.Add(gvc1);
GridViewColumn gvc2 = new GridViewColumn();
gvc2.DisplayMemberBinding = new Binding("LastName");
gvc2.Header = "Last Name";
gvc2.Width = 100;
myGridView.Columns.Add(gvc2);
GridViewColumn gvc3 = new GridViewColumn();
gvc3.DisplayMemberBinding = new Binding("EmployeeNumber");
gvc3.Header = "Employee No.";
gvc3.Width = 100;
myGridView.Columns.Add(gvc3);
//ItemsSource is ObservableCollection of EmployeeInfo objects
myListView.ItemsSource = new myEmployees();
myListView.View = myGridView;
myStackPanel.Children.Add(myListView);

i'd try following approach:
A) you need to have the list box display grid view - i believe this you've done already
B) define a style for GridViewColumnHeader:
<Style TargetType="{x:Type GridViewColumnHeader}" x:Key="gridViewColumnStyle">
<EventSetter Event="Click" Handler="OnHeaderClicked"/>
<EventSetter Event="Loaded" Handler="OnHeaderLoaded"/>
</Style>
in my case, i had a whole bunch of other properties set, but in the basic scenario - you'd need Loaded event. Clicked - this is useful if you want to add sorting and filtering functionality.
C) in your listview code, bind the template with your gridview:
public MyListView()
{
InitializeComponent();
GridView gridViewHeader = this.listView.View as GridView;
System.Diagnostics.Debug.Assert(gridViewHeader != null, "Expected ListView.View should be GridView");
if (null != gridViewHeader)
{
gridViewHeader.ColumnHeaderContainerStyle = (Style)this.FindResource("gridViewColumnStyle");
}
}
D) then in you OnHeaderLoaded handler, you can set a proper template based on the column's data
void OnHeaderLoaded(object sender, RoutedEventArgs e)
{
GridViewColumnHeader header = (GridViewColumnHeader)sender;
GridViewColumn column = header.Column;
//select and apply your data template here.
e.Handled = true;
}
E) I guess you'd need also to acquire ownership of ItemsSource dependency property and handle it's changed event.
ListView.ItemsSourceProperty.AddOwner(typeof(MyListView), new PropertyMetadata(OnItemsSourceChanged));
static void OnItemsSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
MyListView view = (MyListView)sender;
//do reflection to get column names and types
//and for each column, add it to your grid view:
GridViewColumn column = new GridViewColumn();
//set column properties here...
view.Columns.Add(column);
}
the GridViewColumn class itself doesn't have much properties, so you might want to add some information there using attached properties - i.e. like unique column tag - header most likely will be used for localization, and you will not relay on this one.
In general, this approach, even though quite complicated, will allow you to easily extend your list view functionality.

Have a DataTemplateselector to select one of the predefined templates(Of same DataType) and apply the selector on to the ListView. You can have as many DataTemplates with different columns.

You can use a DataTemplateSelector to return a DataTemplate that you have created dynamically in code. However, this is a bit tedious and more complicated than using a predefined one from XAML, but it is still possible.
Have a look at this example: http://dedjo.blogspot.com/2007/03/creating-datatemplates-from-code.html

From experience I can recommend steering clear of dynamic data templates if you can help it... rather use the advice given here to explictly create the ListView columns, rather than trying to create a DataTemplate dynamically.
Reason is that the FrameworkElementFactory (or whatever the class name is for producing DataTemplates at run time) is somewhat cludgey to use (and is deprecated in favor of using XAML for dynamic templates) - either way you take a performance hit.

This function will bind columns to a specified class and dynamically set header, binding, width, and string format.
private void AddListViewColumns<T>(GridView GvFOO)
{
foreach (System.Reflection.PropertyInfo property in typeof(T).GetProperties().Where(p => p.CanWrite)) //loop through the fields of the object
{
if (property.Name != "Id") //if you don't want to add the id in the list view
{
GridViewColumn gvc = new GridViewColumn(); //initialize the new column
gvc.DisplayMemberBinding = new Binding(property.Name); // bind the column to the field
if (property.PropertyType == typeof(DateTime)) { gvc.DisplayMemberBinding.StringFormat = "yyyy-MM-dd"; } //[optional] if you want to display dates only for DateTime data
gvc.Header = property.Name; //set header name like the field name
gvc.Width = (property.Name == "Description") ? 200 : 100; //set width dynamically
GvFOO.Columns.Add(gvc); //add new column to the Gridview
}
}
}
Let's say you have a GridView with Name="GvFoo" in your XAML, which you would like to bind to a class FOO.
then, you can call the function by passing your class "FOO and GridView "GvFoo" as arguments in your MainWindow.xaml.cs on Window loading
AddLvTodoColumns<FOO>(GvFoo);
your MainWindow.xaml file should include the following
<ListView x:Name="LvFOO">
<ListView.View>
<GridView x:Name="GvTodos"/>
</ListView.View>
</ListView>

Related

WPF edit autogenerated column header text

I'm using a WPF DataGrid to display DataTable's.
I need to be able to edit this bound DataTables (Two-Way Binding).
I'm using the DataGrid as followed:
<DataGrid SelectionUnit="CellOrRowHeader" IsReadOnly="False" AutoGenerateColumns="True" ItemsSource="{Binding Path=SelectedItem.BindableContent, FallbackValue={x:Null}}" />
The Problem I have, the user can't edit the ColumnHeader's like cell content or rows.
The Screenshot below illustrates that porblem. The only thing I can do is sort the columns.
Is there a way to edit the column headers too, for example when the user clicks twice, or presses F2.
Maybe some Style' or a HeaderTemplate will do the job? I have already tried some styles and control templates I've found around the internet, but without any success.
EDIT:
I managed to display the column headers in a TextBox (and not in a TextBlock) within the AutogeneratingTextcolumn event handler:
private void _editor_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e) {
// First: create and add the data template to the parent control
DataTemplate dt = new DataTemplate(typeof(TextBox));
e.Column.HeaderTemplate = dt;
// Second: create and add the text box to the data template
FrameworkElementFactory txtElement =
new FrameworkElementFactory(typeof(TextBox));
dt.VisualTree = txtElement;
// Create binding
Binding bind = new Binding();
bind.Path = new PropertyPath("Text");
bind.Mode = BindingMode.TwoWay;
// Third: set the binding in the text box
txtElement.SetBinding(TextBox.TextProperty, bind);
txtElement.SetValue(TextBox.TextProperty, e.Column.Header);
}
But I couldn't manage to set the binding correctly, if i edit the Text in the TextBoxes, it does not change the text in the Column.Header-Property (which is auto-generated by a binding to a DataTable like explained above).
You forgot to set the source of your binding and you mustn't set the value after the registration of the binding. The correct code would be the following:
private void asdf_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
DataTemplate dt = new DataTemplate(typeof(TextBox));
e.Column.HeaderTemplate = dt;
FrameworkElementFactory txtElement =
new FrameworkElementFactory(typeof(TextBox));
dt.VisualTree = txtElement;
Binding bind = new Binding();
bind.Path = new PropertyPath("Header");
bind.Mode = BindingMode.TwoWay;
// set source here
bind.Source = e.Column;
txtElement.SetBinding(TextBox.TextProperty, bind);
// You mustn't set the value here, otherwise the binding doesn't work
// txtElement.SetValue(TextBox.TextProperty, e.Column.Header);
}
Additionally you must change the binding property to Header, because you are adding the binding to the text property of the TextBox.

TextBox inside a listview cell

I have a listview that I would like to add a textbox inside each gridview column cell so I can type data into it and then fetch that data.
I'm creating a datatemplate and passing it to a cell template for the GridViewColumn but when I look at the listview I can't add anything to the cell. It doesn't look like the textbox was even created.
GridViewColumn conceptColumn = new GridViewColumn();
conceptColumn.Header = conceptName;
conceptColumn.CellTemplate = this.GetDataTemplate();
this.TestModeler.Columns.Add(conceptColumn);
conceptColumn.DisplayMemberBinding = new Binding(conceptName);
private DataTemplate GetDataTemplate()
{
DataTemplate dt = new DataTemplate(typeof(TextBox));
FrameworkElementFactory txtElement = new FrameworkElementFactory(typeof(TextBox));
dt.VisualTree = txtElement;
Binding bind = new Binding();
bind.Path = new PropertyPath("Text");
bind.Mode = BindingMode.TwoWay;
txtElement.SetBinding(TextBox.TextProperty, bind);
txtElement.SetValue(TextBox.TextProperty, "test");
return dt;
}
Please take a look at the ListView Class page at MSDN where you can find a XAML example and plenty of link on how to do various things with a WPF ListView.
Of particular interest to you, please take a look at the How to: Use Templates to Style a ListView That Uses GridView page there which explains what you are trying to do (but in XAML) with examples.
MSDN should always be your first place to look as it is full of information just waiting to be read.

WPF - Display Grid of Results With Dynamic Columns/Rows

I'm querying an online service (google data feed) which can return results that will have different numbers of columns and rows with each request.
So far I have been unable to get the data grid or grid to work for me. Ideally I want something that works like excel - you can just add rows and set the values for an individual cell
You can create a class e.g. myGridCol that represents the column and create a collection of the columns. Read the google data feed and create the columns. Then you need to add the columns individually e.g. myGridCol[0], myGridCol[1] .. as DataGridColumns in the code behind. You cannot bind directly to a column collection.
You simply bind to a collection for the rows that has a collection for the columns.
In my case I am using a GridView but I have used the same approach with DataGrid
In my case sDocs is an ObservableCollection
sDoc has public List DocFields
The Fields collection is exactly the same in each sDoc because I made sure it was.
If the Fields collection is not the same in each sDoc then it does not like that.
sDocs is the ItemsSource for the GridView
Then in the code behind I add the columns. As I said before you cannot bind directly to a columns collection. Notice you can even bind the Path to a Property (.e.g. DispValueShort). My class for DocField has other Properties and methods. DocField is actually an Abstract Class with and Abstract Property DispValueShort. Then I have classes for string, date, and enumeration that implement DocField because edit of a string is different from edit of a date. I even have classes for single value and multi value. This is a stable production application.
Binding
<ListView Grid.Row="1" Grid.Column="0" x:Name="lvSrchResulsGrid"
ItemsSource="{Binding Path=MyGabeLib.Search.SDocs}"
Code behind
sDocBaseResultDocsFieldsIndex = 0;
foreach (GabeLib.DocField docField in sDocBaseResultDocsFields)
{
// Debug.WriteLine(" sDocBaseResultDocsFields DispName = " + docField.FieldDef.DispName);
if (fd.FieldDef == docField.FieldDefApplied.FieldDef)
{
gvc = new GridViewColumn();
gvch = new GridViewColumnHeader();
gvch.Content = fd.FieldDef.DispName;
gvch.HorizontalContentAlignment = System.Windows.HorizontalAlignment.Stretch;
if (fd.FieldDef.Sort)
{
gvch.Click += new RoutedEventHandler(SortClick);
gvch.Tag = fd.FieldDef.Name;
}
if (!fd.AppliedDispGrid) gvc.Width = 0; // how to hide
gvc.Header = gvch;
gvBinding = new Binding();
gvBinding.Mode = BindingMode.OneWay;
gvBinding.Path = new PropertyPath("DocFields[" + sDocBaseResultDocsFieldsIndex.ToString() + "].DispValueShort");
template = new DataTemplate();
textblock = new FrameworkElementFactory(typeof(TextBlock));
textblock.SetValue(TextBlock.TextProperty, gvBinding);
textblock.SetValue(TextBlock.TextTrimmingProperty, TextTrimming.WordEllipsis);
// <Setter Property="TextTrimming" Value="WordEllipsis" />
template.VisualTree = new FrameworkElementFactory(typeof(Grid));
template.VisualTree.AppendChild(textblock);
gvc.CellTemplate = template;
gvSearchResults.Columns.Add(gvc);
break;
}
sDocBaseResultDocsFieldsIndex++;
}

WPF Datagrid autogenerated columns

I have bound a datatable to a datagrid in WPF. Now on clicking a row in the grid I need to have a window pop up. But for that, I need to first change a column in the datagrid to be a hyperlink. Any ideas on how to do that?
<DataGrid Name="dgStep3Details" Grid.Column="1" Margin="8,39,7,8" IsReadOnly="True" ItemsSource="{Binding Mode=OneWay, ElementName=step3Window,Path=dsDetails}" />
If I can't change an autogenerated column to hyperlink, is there a way to add a button to each row instead?
Thanks
Nikhil
So, it was really hard to create hyperlink columns to autogenerated datagrid. What I eventually did was this - create buttons to the grid on the fly and then attach a routed event for the same based on the autogenerate event of the datagrid where I shall put my code. I didn't want my code to be hardcoded to the columns and now I'm flexible by changing the datatable on the fly. Here is the code:
private void dgStep3Details_AutoGeneratedColumns(object sender, EventArgs e)
{
DataGrid grid = sender as DataGrid;
if (grid == null)
return;
DataGridTemplateColumn col = new DataGridTemplateColumn();
col.Header = "More Details";
FrameworkElementFactory myButton = new FrameworkElementFactory(typeof(Button), "btnMoreDetails");
myButton.SetValue(Button.ContentProperty, "Details");
myButton.AddHandler(Button.ClickEvent, new RoutedEventHandler(btnMoreDetails_Click));
DataTemplate cellTempl = new DataTemplate();
//myButton.SetValue(Button.CommandParameterProperty, ((System.Data.DataRowView)((dgStep3Details.Items).CurrentItem)).Row.ItemArray[0]);
cellTempl.VisualTree = myButton;
col.CellTemplate = cellTempl;
dgStep3Details.Columns.Add(col);
}
public void btnMoreDetails_Click(object sender, RoutedEventArgs e)
{
//Button scrButton = e.Source as Button;
string currentDetailsKey = ((System.Data.DataRowView)(dgStep3Details.Items[dgStep3Details.SelectedIndex])).Row.ItemArray[0].ToString();
// Pass the details key to the new window
}
I don't think you'll be able to get these advanced UI features out of autogenerated columns. I think you'll either have to decide to program these columns in C# or VB.NET when you retrieve your data and tailor them the way you like, or you'll have to abandon the UI ideas you've mentioned. Autogenerated columns just cannot do that.
However, you could change your approach. Try checking into events like MouseLeftButtonDown, etc. and see if you can simulate the behavior you want by other means.

DataGridColumn Binding in code

Does anyone know how I can do the equivalent XAML binding in code?
<DataGrid ... >
<DataGrid.Columns>
<DataGridTextColumn
Binding="{Binding Description}" <=== set in code **
/>
</DataGrid.Columns>
</DataGrid>
Cheers,
Berryl
=== UPDATE ====
It looks like the method I have been looking for is DataGridColumn.GenerateElement
If so, then the focus of this question is now how to set the Binding correctly. The reason I want to do this code is that my grid has 7 columns that are identical visually and whose data can be known by an index.
So I want to be able to simplify the xaml by using a subclass DataGridTextColumn which has an index property, and just have:
<DataGrid ... >
<DataGrid.Columns >
<local:DayOfWeekColumn Index="0" />
<local:DayOfWeekColumn Index="1" />
....
<local:DayOfWeekColumn Index="7" />
</DataGrid.Columns >
</DataGrid >
=== REVISED QUESTION ===
Assuming the Binding itself is logically and syntactically correct, what should the parameters to BindingOperations.SetBinding be??
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) {
var activity = (ActivityViewModel)dataItem;
var cellData = activity.Allocations[Index];
var b = new Binding
{
Source = cellData,
UpdateSourceTrigger = UpdateSourceTrigger.LostFocus,
Converter = new AllocationAmountConverter()
};
BindingOperations.SetBinding(??, ??, b);
return ??;
}
=== EDITS for ARAN =====
I am not overriding GenerateElement right now, but rather trying to get a static helper to set my binding for me. The helper is needed in any event to compensate for not being able to bind Header content in the current implementation of MSFT's DataGrid.
Basically the idea is to catch the DC from the grid and use it as necessary on each of the columns, which in this case would be the Header content, cell style, and Binding. Here is the code:
public class TimesheetDataGridColumnContextHelper
{
static TimesheetDataGridColumnContextHelper() {
FrameworkElement.DataContextProperty.AddOwner(typeof (DataGridTextColumn));
FrameworkElement.DataContextProperty.OverrideMetadata(
typeof (DataGrid),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, OnDataContextChanged));
}
public static void OnDataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var grid = d as DataGrid;
if (grid == null || !grid.Name.Equals("adminActivityGrid")) return;
foreach (var col in grid.Columns) {
var dowCol = col as DayOfTheWeekColumn;
if (dowCol == null) continue;
var context = (IActivityCollectionViewModelBase) e.NewValue;
var index = Convert.ToInt32(dowCol.DowIndex);
_setHeader(dowCol, context, index);
var editStyle = (Style) grid.FindResource("GridCellDataEntryStyle");
dowCol.CellStyle = editStyle;
_setBinding(dowCol, index, context);
}
}
private static void _setBinding(DayOfTheWeekColumn dowCol, int index, IActivityCollectionViewModelBase context) {
dowCol.Binding = new Binding
{
Path = new PropertyPath(string.Format("Allocations[{0}]", index)),
UpdateSourceTrigger = UpdateSourceTrigger.LostFocus,
Converter = new AllocationAmountConverter()
};
}
private static void _setHeader(DataGridColumn col, IActivityCollectionViewModelBase context, int index)
{
var date = context.HeaderDates[index];
var tb = new TextBlock
{
Text = date.ToString(Strings.ShortDayOfWeekFormat),
ToolTip = date.ToLongDateString()
};
col.Header = tb;
}
}
}
Everything works except for the Binding. I can't tell if it's because my binding is wrong somehow (although I get no obvious errors) or this is not a good place to set it. The grid columns are just empty when I run it.
Any idea??
Cheers,
Berryl
=== FIXED! ===
The logic in the last update was actually correct, but getting lost in the internals of the DataGrid I missed that my Binding.Path was missing the property to be bound to! Credit to Aran for understanding the issue, realizing that GenerateElement overrides were not necessary, and catching that the Binding Source should not have been set.
You're always doing the fiddly grid bits eh Beryl?
Do a couple of things. Use reflector to look at the implementation of GenerateElement in the DataGridTextColumn. (.NET programmers live in reflector)
Now for the answer:
In the datagrid each column is not part of the visual tree. The column has two methods GenerateElement and GenerateEditingElement. These methods return the viewer and the editor for the cell respectively. In your method above you are not creating the viewer, which will probably be a TextBlock.
from reflector, the implementation of GenerateElement is as below, notice the first thing they do is create the viewer for the cell.
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
TextBlock e = new TextBlock();
this.SyncProperties(e);
base.ApplyStyle(false, false, e);
base.ApplyBinding(e, TextBlock.TextProperty);
return e;
}
Once you have a textblock you can use the line below to set the binding on it.
BindingOperations.SetBinding(textBlock, TextBlock.TextProperty, binding);
I am not however convinced that you actually need to override the GenerateElement and GenerateEditingElement to get your desired effect. I think you could overide the Binding property of the base class and just modify the binding there with your extra field whenever it is set. This will mean everything else will just work and you wont end up removing functionality from your column. Once again a crawl through reflector looking at the class DataGridBoundColumn (the abstract base class) would be beneficial.
I do something similiar in one of our columns whenever a binding is set I modify the clipboard binding by adding an extra property so I can copy and paste effectively.
EDIT: Update...this should probably be another question but..
You are explicitly setting the source of the binding in your setBinding method. In the grid the source of the binding is the data contained in the row. You are setting it, which means it would be the same for each row. You can apply these funky bindings without the source property before the data context is set, the source becomes the item in each row, and your binding should reflect an index into the property held in each row.
Based on MSDN, it sounds like the first parameter of SetBinding() should be the control that you want to display the binding in (this in this case, assuming that GenerateElement() is a member of the DayOfWeekColumn class), and the second property is the property to bind the data to. I haven't used the WPF DataGrid very much, but I didn't see anything like a text property to set.
I do see that the DataGridTextColumn does have a Binding property, though. Maybe it would work to set it to the binding you created manually above?

Resources