WPF - Display Grid of Results With Dynamic Columns/Rows - wpf

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++;
}

Related

Tricky binding in WPF

I have a list of countries and a list of translations in different languages for each country, this is available in my ViewModel as Countries and Translations.
I want to display each country and its translations in ONE datagrid row.
Each country may have different translations filled.
The datagrid I am using allows to add new columns at runtime.
I managed to show this using a multibinding:
foreach (var language in languages)
{
Binding translationsBinding = new Binding { Path = new PropertyPath("Translations") };
Binding languageBinding = new Binding { Source = language.ID };
MultiBinding multiBinding = new MultiBinding();
multiBinding.Converter = new TranslationsMultiConverter();
multiBinding.Mode = BindingMode.TwoWay;
multiBinding.Bindings.Add(translationsBinding);
multiBinding.Bindings.Add(languageBinding);
}
My question is: how can I set a changed translation back to my Translations list, by using a binding only?
Regards

Dynamically add a column to datagrid or generate datagrid with specific columns from a list

I've been struggling with datagrid databinding for days now..
I am trying to generate datagrid dynamically from a list of custom objects.
Because the number of list-object properties that I need to display might change, I don't know exact number of columns, nor their headers.
So I thought, I'd dynamically add only the columns I need and create bindings (to the specific object properties) for them. After that I would fill in the datagrid with data from a list based on the bindings.
For example:
I create an empty datagrid
<DataGrid x:Name="MyDataGrid" AutoGenerateColumns="false" ItemsSource="{Binding}" Margin="0,0,0,31" Grid.RowSpan="2">
</DataGrid>
I create my list, to store my data and the data object:
Public Class Res
Public Shared TableData As New List(Of DataItem)
End Class
Public Class DataItem
Public Property Name() As String
Public Property MyProperty() As String
Public Property Prop() As String
Public Property Prop1() As String
End Class
Now, say I want to show only the Name column in my datagrid, so I add a column with specific binding (presumably this is where the problem lies, yet I can't figure it out):
Dim col As New DataGridTextColumn
Dim bb As New Binding
bb.Path = New PropertyPath("Name")
bb.Mode = BindingMode.TwoWay
bb.Source = Res.TableData
col.Binding = bb
col.Header = "Name"
MyDataGrid.Columns.Add(col)
Then, I finally create and add some data to my list:
Dim entry As New DataItem
entry.Name = "Test Name"
entry.MyProperty = "Test Property"
entry.Prop = "one Entry"
Res.TableData.Add(entry)
Me.MyDataGrid.ItemsSource = Res.TableData
Now the result is, that the entire column is filled with that same data. Where my intention was to fill only the row that corresponds to the exact list item. For example, if I later want to add another item to the list with a different "Name" it should be shown in the datagrid as another row with a different "Name" as well.
I would suggest you to bind your grid to your List property, have AutoGenerateColumns = True and listen to AutoGeneratingColumn there you decide if you want to add that column to grid or not.

Convert DataGrid cell content

I have a DataGrid whose ItemsSource is set to a DataTable. The DataTable has a column of type DateTime, and I would like to display informational text (ie. "N/A") if the date in a particular cell is a certain value.
My first thought was to somehow bind the cell content to itself, and use a converter, but I can't seem to get it working correctly, and it seems as though there should be a better way.
Additionally, both the DataGrid and DataTable are dynamically generated, so this has to be done in the code behind.
Here's the code I tried initially:
// Create a new DataGridCellStyle
Style myStyle = new Style();
myStyle.TargetType = typeof(DataGridCell);
// Create the binding
Binding myBinding = new Binding();
myBinding.RelativeSource = RelativeSource.Self;
myBinding.Converter = new DateTimeToStringConverter();
// Add the Content setter
Setter mySetter = new Setter();
mySetter.Property = ContentProperty;
mySetter.Value = myBinding;
myStyle.Setters.Add(setter);
// Set the Style and ItemsSource
myDataGrid.CellStyle = myStyle ;
myDataGrid.ItemsSource = myDataTable.DefaultView;
DateTimeToStringConverter does implement IValueConverter, but I'm guessing the problem lies somewhere with the binding, since DateTimeToStringConverter is never actually called when the DataGrid is displayed.
At first, you Add the variable with the name setter to the Setters collection, but you are define the variable with the name mySetter. It may be a reason, why your Converter is not actually called.
Also the solution for your problem will be a bit more complicated.
Actually the Convert get a value of type RowDataView which contains a data for whole row. There is not an information in the converter about a Column or a Cell that is actually binded.
Better will be skip AutoGenerateColumns and generate them programmatically.
Here is example:
myDataGrid.ItemsSource = myDataTable.DefaultView;
myDataGrid.AutoGenerateColumns = false;
foreach (DataColumn column in myDataTable.Columns)
{
Binding binding = new Binding(column.ColumnName)
{
Converter = new DateTimeToStringConverter()
};
DataGridColumn gridColumn = new DataGridTextColumn
{
SortMemberPath = column.ColumnName,
Header = column.ColumnName,
Binding = binding
};
myDataGrid.Columns.Add(gridColumn);
}
Of course, for a performance will be better use the converter only in a DateTime columns.

Automatically create an input UI from db fields in winforms?

I'm trying to create a form in winforms to add records to a db, a new customer for example. I'm working with entity framework.
What I did until today is to create a new empty "Customer" object from the class that the entity framework generated. Then I added this empty object to a list and set the list as the datasource of a datagridview.
That way I automatically had in the grid all the required fields to input to the db.
Everything worked.
But now, the client wants a better design for the UI - something that looks like a contact form in web pages and not a grid row.
How can I make something like that automatically, like I had with the datagridview, creating all the input fields automatically according to the db structure without creating manually labels and textboxes?
Your best bet would be to keep the DataGridView but override the style so that it looks far from a grid and more like what your boss is expecting.
Some suggestions to achieve this:
Remove the lines between each row.
Remove the headers and grid borders.
Add lots of padding to each row and column so each entry is spaced
out. For more advanced stuff you may need to override the Paint
method of some of the controls of the grid.
I ended up iterating the grid and creating textboxes and labels in each iteration.
void Generate_TextBoxes()
{
// top of textboxes
int current_top=150;
int current_left = 1000;
// index used to match between each textbox and the properate column in grid
int my_index = -1;
// iterate the grid and create textbox for each column
foreach (DataGridViewColumn col in dataGridView_add_customer.Columns)
{
my_index++;
// generate textboxes only for visible columns
if (col.Visible == true)
{
// increase the top each time for space between textboxes
current_top += 40;
// create a second column of textboxes (not all of them in 1 long column)
if (my_index == 6) { current_top = 190; current_left = 450; }
TextBox t = new TextBox();
t.Top = current_top;
t.Left = current_left;
t.Width = 170;
t.TextChanged +=new EventHandler(t_TextChanged);
// give an 'id' for each textbox with the corresponding index of the grid
t.Name = my_index.ToString();
Label l = new Label();
l.Text = col.HeaderCell.Value.ToString();
l.Top = current_top;
l.Left = current_left + 190;
this.Controls.Add(l);
this.Controls.Add(t);
}
and the function that binds the textbox to the grid:
void t_TextChanged(object sender, EventArgs e)
{
// create a reference in order to be able to access grid properties such as rows..
TextBox tt = (TextBox)sender;
// access the correct cell in the grid using the Name property you gave to the textbox (it was name=current_index..)
dataGridView_add_customer.Rows[0].Cells[int.Parse(tt.Name)].Value = tt.Text;
}

Create WPF ItemTemplate DYNAMICALLY at runtime

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>

Resources