I have a DataGrid backed by an Observable collection of Dictionary. I'd like to perform edits via the grid and currently the UI doesn't seem to allow it. I also observe that when adding the IEditableObject interface to the Dictionary, the interface methods are called when a cell is clicked, but nothing happens on the UI itself.
Here's my minimum example:
class MainWindowViewModel
{
public MainWindowViewModel() {
ItemsSource = new ObservableCollection<DataGridData> {
new DataGridData() {{"Name", "Abe"},{"Age", "50"},{"Gender", "Male"}},
new DataGridData() {{"Name", "Shelly"},{"Age", "20"},{"Gender", "Female"}
}
};
}
public ObservableCollection<DataGridData> ItemsSource { get; set; }
}
public class DataGridData : Dictionary<string, string>, IEditableObject {
public void BeginEdit(){}
public void CancelEdit(){}
public void EndEdit(){}
}
public partial class MainWindow {
public MainWindow() {
DataContext = new MainWindowViewModel();
InitializeComponent();
foreach (var col in ViewModel.ItemsSource[0].Keys) {
AddColumns(col, col);
}
}
MainWindowViewModel ViewModel {
get { return DataContext as MainWindowViewModel; }
}
void AddColumns(string id, string name)
{
FrameworkElementFactory textBlock = new FrameworkElementFactory(typeof(TextBlock));
textBlock.SetValue(TextBlock.PaddingProperty, new Thickness(2));
textBlock.SetValue(TextBlock.TextProperty, new Binding(string.Format("[{0}]", id)));
Binding textDecorationBinding = new Binding();
textDecorationBinding.ElementName = "DataGrid";
textDecorationBinding.Path = new PropertyPath("DataContext.TextDecoration");
textBlock.SetValue(TextBlock.TextDecorationsProperty, textDecorationBinding);
DataTemplate cellTemplate = new DataTemplate();
cellTemplate.VisualTree = textBlock;
DataGridTemplateColumn column = new DataGridTemplateColumn();
column.Header = name;
column.SortMemberPath = name;
column.CellTemplate = cellTemplate;
DataGrid.Columns.Add(column);
}
}
<Window x:Class="Datagrid.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid
Name="DataGrid"
ItemsSource="{Binding ItemsSource}"
AutoGenerateColumns="False"
>
</DataGrid>
</Grid>
</Window>
The dictionary is used because I don't anything about the column data at compile time. The ItemsSource above is just an example but I could have any number of key/values at run time.
What changes are necessary to enable editing the cell data?
Edit
I've never used it, but I'm reading about using reflection to emit an actual class that could replace the dictionary key/values. I may give this a go unless someone has contrary advice.
In my opition your code is a little bit "tortuous". Indeed I wouldn't use a Dictionary as model of the objects which I want to handle. In my opinion it is better to use a specialized class (in this way you can implement the "famous" INotifyPropertyChanged interface).
For example you can use a Person class (here a very fast implementation of it):
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Genders Gender { get; set; }
}
You can implement IEditableObject, but it is not mandatory.
Last (but not the least) if I were you, I will declare DataGrid columns in my XAML and not in my code (I can't see a valid reason for using code in your situation; I don't know maybe you need to have dynamic columns. In this case you can read this very good article).
So my ViewModel is (you can easy replace Dictionary with Person class):
public class MainWindowViewModel
{
public MainWindowViewModel()
{
People = new ObservableCollection<Dictionary<string, string>> {
new Dictionary<string, string>() {{"Name", "Abe"}, {"Age", "50"}, {"Gender", "Male"}},
new Dictionary<string, string>() {{"Name", "Shelly"}, {"Age", "20"}, {"Gender", "Female"}}
};
}
public ObservableCollection<Dictionary<string, string>> People { get; private set; }
}
My XAML (if you replaced Dictionary with Person, remove the square brackets from the columns bidings):
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<StackPanel Orientation="Vertical">
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Path=People}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=[Name]}" Header="Name" Width="2*" />
<DataGridTextColumn Binding="{Binding Path=[Age]}" Header="Age" Width="*" />
<DataGridTextColumn Binding="{Binding Path=[Gender]}" Header="Gender" Width="*" />
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Window>
My window code-behind:
namespace WpfApplication1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
}
}
In this way, everything works fine and you can edit data by using the DataGrid.
I hope this sample can help you.
EDIT
If you need to use your approach, probably in your AddColumns method you are missing to declare a CellEditingTemplate. Your method will become:
private void AddColumns(string id, string name)
{
FrameworkElementFactory textBlock = new FrameworkElementFactory(typeof(TextBlock));
textBlock.SetValue(TextBlock.PaddingProperty, new Thickness(2));
textBlock.SetValue(TextBlock.TextProperty, new Binding(String.Format("[{0}]", id)));
FrameworkElementFactory textBox = new FrameworkElementFactory(typeof(TextBox));
textBox.SetValue(TextBox.PaddingProperty, new Thickness(2));
textBox.SetValue(TextBox.TextProperty, new Binding(String.Format("[{0}]", id)));
Binding textDecorationBinding = new Binding();
textDecorationBinding.ElementName = "DataGrid";
textDecorationBinding.Path = new PropertyPath("DataContext.TextDecoration");
textBlock.SetValue(TextBlock.TextDecorationsProperty, textDecorationBinding);
DataTemplate cellTemplate = new DataTemplate();
cellTemplate.VisualTree = textBlock;
DataTemplate cellEditingTemplate = new DataTemplate();
cellEditingTemplate.VisualTree = textBox;
DataGridTemplateColumn column = new DataGridTemplateColumn();
column.Header = name;
column.SortMemberPath = name;
column.CellTemplate = cellTemplate;
column.CellEditingTemplate = cellEditingTemplate;
DataGrid.Columns.Add(column);
}
I read that you are thinking to use reflection to emit an actual class that could replace the dictionary key/values. Of course this is a solution, but I suggest to consider ICustomTypeDescriptor or CustomTypeDescriptor.
Related
I have the following question:
I'm having a couple of Pages, one of the pages is called: StartschermCursussen.xaml and is used in a frame inside my MainWindow.xaml.
MainWindow.xaml
<Window x:Class="ClientWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ClientWPF"
mc:Ignorable="d"
Loaded="Window_Loaded"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Frame Name="sideWindow" HorizontalAlignment="Left" Width="50" Panel.ZIndex="50"/>
<Frame Name="mainWindow" Margin="50,0,0,0" Source="Pages/StartschermCursussen.xaml" NavigationUIVisibility="Hidden"/>
</Grid>
</Window>
Inside my StartschermCursussen.xaml.cs I'm having my Datacontext that is connected to an 'Container' class that holds references and has properties that are connected to my ViewModels.
StartschermCursussen.xaml.cs
public partial class StartschermCursussen : Page
{
public StartschermCursussen()
{
InitializeComponent();
KeepAlive = true;
DataContext = Container;
}
public ItemsContainer Container { get; set; }
}
Container class
public class ItemsContainer
{
public FilterItem FilterItem { get; set; }
public List<CourseDTO> courses { get; set; } = new List<CourseDTO>();
public List<IconItem> IconList { get; set; } = new List<IconItem>();
public ItemsContainer()
{
//One of my ViewModels
FilterItem = new FilterItem();
}
}
Now, I'm having another page called DetailsCursus.xaml.cs. In that page I'm trying to get specific properties from my ViewModels through databinding in xaml to apply changes to them and show them to the end-user etc.
One way how I did this is through constructor overloading inside my StartschermCursussen.xaml.cs.
StartschermCursussen.xaml.cs (Sending data from DataContext -> DetailsCursus.xaml.cs)
private void menuItemDetails_Click(object sender, RoutedEventArgs e)
{
MenuItem menuItem = (MenuItem)sender;
var contextMenu = (ContextMenu)menuItem.Parent;
var item = (DataGrid)contextMenu.PlacementTarget;
CourseDTO toDeleteFromBindedList = (CourseDTO)item.SelectedCells[0].Item;
//Here I'm sending the data through constructor
this.NavigationService.Navigate(new DetailsCursus(toDeleteFromBindedList, Container.IconList));
}
DetailsCursus.xaml.cs
public DetailsCursus(CourseDTO data, List<IconItem> iconList)
{
object[] items = { data, iconList };
InitializeComponent();
DataContext = items;
}
In the above code snippet I put incoming data in an Array and then linked it to my Datacontext. But I have the feeling that this is not the correct way to get data from your Datacontext.
The code snippet below shows how I get to the data in the Datacontext and I show this in the ItemSource of a ComboBox. It works and returns me the results but I feel like I'm doing this the wrong way.
DetailsCursus.xaml
<ComboBox Width="100" Height="30" Grid.Row="1" HorizontalAlignment="Right" Margin="5" ItemsSource="{Binding Path=[1]}" DisplayMemberPath="IconName" />
Would someone please explain to me how to correctly use the Datacontext within the WPF Pages.
Any kind of help is greatly appreciated!
object[] is certainly unconventional choice for DataContext.
You can create a special named class for DetailsCursus view (pretty common, must-do in MVVM pattern), which will do wonders to readability:
public class DetailsCursusViewModel
{
public CourseDTO SelectedCourse { get; set; }
public List<IconItem> IconList { get; set; }
}
private void menuItemDetails_Click(object sender, RoutedEventArgs e)
{
var menuItem = (MenuItem)sender;
var contextMenu = (ContextMenu)menuItem.Parent;
var dg = (DataGrid)contextMenu.PlacementTarget;
var toDeleteFromBindedList = (CourseDTO)dg.SelectedCells[0].Item;
var viewModel = new DetailsCursusViewModel
{
SelectedCourse = toDeleteFromBindedList ,
IconList = Container.IconList
};
var page = new DetailsCursus(viewModel);
this.NavigationService.Navigate(page);
}
public DetailsCursus(DetailsCursusViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel; // fix bindings after this!
}
I also suggest to explore mvvm pattern in WPF, and also check alternatives for navigation (UserControl VS Page in WPF)
So I have a bunch of classes that are derived from some base class. I have classes (collectors) that have a methods that returns collections of these classes. I also have a TabControl where each tab has custom control that contains a DataGrid. There is a ViewModel for these custom controls. In a ViewModel I have collection of base class elements that are returned by collectors. I want to bind DataGrids to these collections and generate columns automatically, but derived classes have different properties and base class doesn't have any properties that should be shown.
internal class ElementsInfoViewModel : INotifyPropertyChanged
{
private readonly ElementScaner _elementScaner;
public ElementsInfoViewModel(ElementScaner elementScaner)
{
_elementScaner = elementScaner;
}
public ReadOnlyCollection<SystemElement> ShownElements => _elementScaner.Elements;
}
<UserControl x:Class="SysSpy.Desktop.Controls.ElementsInfoTabItemContent"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:toolkit="http://schemas.microsoft.com/wpf/2008/toolkit"
xmlns:local="clr-namespace:SysSpy.Desktop.Controls"
mc:Ignorable="d">
<Grid>
<toolkit:DataGrid ItemsSource="{Binding ShownElements}">
</toolkit:DataGrid>
</Grid>
</UserControl>
I removed everything unnecessary from code.
The idea that comes up to my mind is to cast somehow collection of base type to collection of derived type (with IValueConverter possibly), but as collection is updated many times per second, reflection might be bad solution for this
UPD
TabControl is binded to some collection in main vm
internal class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
Test = new ObservableCollection<ElementsInfoViewModel>();
var certificatesCollector = new CertificatesCollector();
var certificatesScaner = new ElementScaner(certificatesCollector, "Certificates");
certificatesScaner.Scan();
var certifVM = new ElementsInfoViewModel(certificatesScaner);
Test.Add(certifVM);
}
public ObservableCollection<ElementsInfoViewModel> Test { get; set;
}
}
<Window x:Class="SysSpy.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SysSpy.Desktop"
xmlns:viewModel="clr-namespace:SysSpy.Desktop.ViewModels"
xmlns:controls="clr-namespace:SysSpy.Desktop.Controls"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<viewModel:MainViewModel x:Key="viewModelSource"/>
</Window.Resources>
<Window.DataContext>
<Binding Source="{StaticResource viewModelSource}"/>
</Window.DataContext>
<DockPanel LastChildFill="True">
<Grid>
<TabControl ItemsSource="{Binding Test}">
<TabControl.ItemTemplate>
<DataTemplate DataType="viewModel:ElementsInfoViewModel">
<controls:ElementsInfoTabItemHeader/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate DataType="viewModel:ElementsInfoViewModel">
<controls:ElementsInfoTabItemContent/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</DockPanel>
</Window>
I don't know if this is what you need, but I have another option.
Since DataGrid doesn't have a method to dinamically bind the columns, you can extend it and make it on your own.
For example in this way:
class MyDataGrid : DataGrid
{
public MyDataGrid() : base()
{
}
protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
{
base.OnItemsSourceChanged(oldValue, newValue);
if (newValue != null)
{
var enumerator = newValue.GetEnumerator();
if (enumerator.MoveNext())
{
Columns.Clear();
var firstElement = enumerator.Current;
var actualType = firstElement.GetType();
foreach (var prop in actualType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => x.CanRead))
{
Columns.Add(new DataGridTextColumn
{
Header = prop.Name,
Binding = new Binding(prop.Name)
});
}
}
}
}
}
I wrote a simple project for test it and it works in my environment.
First object type (Person)
class Person
{
public string Name { get; set; }
public string Surname { get; set; }
public int Age { get; set; }
}
Second object type (Car)
class Car
{
public string Name { get; set; }
public int HP { get; set; }
}
The element you called SystemElement (Title is used as tab header)
class SystemElement
{
public SystemElement(string title, IList<object> elements)
{
Title = title;
Elements = new ReadOnlyCollection<object>(elements);
}
public string Title { get; set; }
public ReadOnlyCollection<object> Elements { get; }
}
ElementScanner that extracts the elements to show. In my test the list is set in the constructor:
class ElementScanner
{
public ElementScanner()
{
var data = new List<SystemElement>();
data.Add(new SystemElement("People", new List<object>
{
new Person { Name = "John", Surname = "Doe", Age = 22},
new Person { Name = "Lenny", Surname = "Pegasus", Age = 30},
new Person { Name = "Duffy", Surname = "Duck", Age = 22}
}));
data.Add(new SystemElement("Cars", new List<object>
{
new Car { Name = "Mercedes", HP = 700 },
new Car { Name = "Red Bull", HP = 650 },
new Car { Name = "Ferrari", HP = 600 }
}));
Elements = new ReadOnlyCollection<SystemElement>(data);
}
public ReadOnlyCollection<SystemElement> Elements { get; }
}
Now we have the ViewModel. In my example I did't use the INotifyPropertyChanged since properties will never change (it's a test project).
class ElementsInfoViewModel
{
public ElementsInfoViewModel()
{
var elementScanner = new ElementScanner();
Elements = elementScanner.Elements;
}
public ReadOnlyCollection<SystemElement> Elements { get; }
}
Now move to the view side. First of all we have the UserControl that rappresents the content of each tab. So it will show a DataGrid of type MyDataGrid:
<UserControl x:Class="Stack.ElementInfoView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Stack"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<local:MyDataGrid ItemsSource="{Binding Elements}" >
</local:MyDataGrid>
</Grid>
</UserControl>
Finally we have the main view that will merge all togather:
<Window x:Class="Stack.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Stack"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:ElementsInfoViewModel />
</Window.DataContext>
<Grid>
<TabControl ItemsSource="{Binding Elements}">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding Title}" />
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ContentTemplate>
<DataTemplate DataType="local:ElementsInfoViewModel">
<local:ElementInfoView></local:ElementInfoView>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
This is what we have (and I suppose it is what you want):
I don't know if it works, but have you tried with Generics?
I mean something like this:
internal class ElementsInfoViewModel<T> : INotifyPropertyChanged
where T : SystemElement
{
public ElementsInfoViewModel(ElementScaner elementScaner)
{
var list = new List<T>();
foreach (var el in elementScanner.Elements)
{
list.Add(el as T);
}
ShownElements = new ReadOnlyCollection<T>(list);
}
public ReadOnlyCollection<T> ShownElements { get; }
}
I write it in the browser, so my code can be wrong, but take the idea.
If it could be nice, the best would be to have the generic version of ElementScanner (something like ElementScanner< T > ).
Let us know if it will work
I have a custom data structure and I need a WPF component which will represent my data structure. Component should look like on a picture. Component should be dynamic, so number of ColumnSets in structure can be from 1 to x. And each columnSet can have different count of Columns.
public class CustomStructure{
public List<ColumnSet> ColumnSets{get; set;}
}
public class ColumnSet{
public string Name { get; set; }
public List<Column> ColumnSets{get;}
}
public class Column{
public string Name { get; set; }
public List<int> Data{get;}
}
First row represent Name property from class ColumnSet. Second row is a Name property from class Column. Other rows are Data from Column class.
Wanted WPF component design (image)
My Solution
I defined two components. First represent outer table, and it is called FuzzyTableControl. It has X columns and only ONE row.
Second represent FuzzyInnerTableControl for each column in first row of FuzzyTableControl.
FuzzyTableControl.xaml
<UserControl:Class="FuzzyTableControl">
<UserControl.DataContext>
<viewModel:FuzzyTableViewModel/>
</UserControl.DataContext>
<DataGrid AutoGenerateColumns="True" IsReadOnly="True"
CanUserAddRows="False"
CanUserDeleteRows="False"
ItemsSource="{Binding DataTable}">
// here is problem 1
<DataGrid.Columns>
<DataGridTemplateColumn Header="{Binding}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<view:FuzzyInnerTableControl/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</UserControl>
FuzzyTableViewModel.cs
public class FuzzyTableViewModel : BaseViewModel
{
public FuzzyTable Table { get; set; }
public DataTable DataTable { get; set; }
public FuzzyTableViewModel()
{
Table = FuzzyTable.Generate(3, 2);
DataTable = new DataTable();
foreach (var attribute in Table.Attributes)
{
DataTable.Columns.Add(new DataColumn(attribute.Name));
}
var row = new List<object>();
foreach (var attribute in Table.Attributes)
row.Add(attribute);
DataTable.Rows.Add(row.ToArray());
}
}
FuzzyInnerTableControl.xaml
this one works good
<UserControl:Class="FuzzyInnerTableControl">
<UserControl.DataContext>
<viewModel:FuzzyInnerTableViewModel/>
</UserControl.DataContext>
<DataGrid ItemsSource="{Binding DataTable}"/>
</UserControl>
FuzzyInnerTableViewModel.cs
public class FuzzyInnerTableViewModel : BaseViewModel
{
public DataTable DataTable { get; }
public ColumnSetDouble ColumnSetDouble { get; set; }
public FuzzyInnerTableViewModel()
{
// test
var table = FuzzyTable.Generate(3, 2);
ColumnSetDouble = table.ClassAttribute;
//end test
DataTable = new DataTable();
foreach (var attribute in ColumnSetDouble.Columns)
DataTable.Columns.Add(new DataColumn(attribute.Name));
for (int rowId = 0; rowId < ColumnSetDouble.Columns[0].Data.Count; rowId++)
{
var row = new List<object>();
foreach (var column in ColumnSetDouble.Columns)
{
row.Add(column.Data[rowId]);
}
DataTable.Rows.Add(row.ToArray());
}
}
}
I don't know how to define cell template for each column of FuzzyTableControl. This solution creates NEW column, but I need load columns dynamically from viewModel.
In order to define the template for all auto-generated cells, you can try using the DataGrid.CellStyle.
<DataGrid AutoGenerateColumns="True" IsReadOnly="True"
CanUserAddRows="False"
CanUserDeleteRows="False"
ItemsSource="{Binding DataTable}">
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<view:FuzzyInnerTableControl/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.CellStyle>
</DataGrid>
If I have a better idea I'll edit.
Now you ask why <view:FuzzyInnerTableControl DataContext="{Binding}"/> is not working. So I suppose you have prepared your outer DataTable so it really contains the FuzzyInnerTableViewModel which is not completely trivial:
// when creating a column, ensure to set the DataType, so your content won't be reduced to some text
DataTable.Columns.Add(new DataColumn(attribute.Name, typeof(FuzzyInnerTableViewModel)));
// I have no idea what attribute is, but it better be a FuzzyInnerTableViewModel for your use case!
row.Add(attribute);
First of all, the DataGrid will try to auto generate relatively useless DataGridTextColumn, which needs to be changed. Handle the AutoGeneratingColumn event in order to change the column type. The DataGridTemplateColumn will not actually provide you with the cell content and if you (like me) try to set the DataGridCell.Content via CellStyle, it will kindly ignore you and set the content locally instead. So lets push the cell value to DataGridCell.Tag for the time being.
Here is FuzzyTableControl with the DataGrid_AutoGeneratingColumn method that is connected to the DataGrid.AutoGeneratingColumn event in xaml.
public partial class FuzzyTableControl : UserControl
{
public FuzzyTableControl()
{
InitializeComponent();
}
private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
e.Column = new DataGridTemplateColumn
{
CellStyle = new Style(typeof(DataGridCell))
{
Setters =
{
new Setter(DataGridCell.TagProperty, new Binding(e.PropertyName)),
}
},
Header = e.Column.Header,
HeaderStringFormat = e.Column.HeaderStringFormat,
HeaderStyle = e.Column.HeaderStyle,
HeaderTemplate = e.Column.HeaderTemplate,
HeaderTemplateSelector = e.Column.HeaderTemplateSelector,
// transfer whatever properties you feel worthy in your scenario
};
}
}
Now, at least the desired data are hanging around somewhere in the Tag, but guess what - the DataGrid.CellStyle was just replaced by a style who's main concern is to dynamically bind some data. So lets move the DataTemplate into the resources and add the binding to some parent DataGridCell.Tag while its hot...
<UserControl.Resources>
<DataTemplate x:Key="FuzzyInnerTableTemplate">
<view:FuzzyInnerTableControl
DataContext="{Binding Tag,RelativeSource={RelativeSource AncestorType=DataGridCell}}"/>
</DataTemplate>
</UserControl.Resources>
and wire up the template with all the other things inside column generation for the DataGridTemplateColumn
// ...
// transfer whatever properties you feel worthy in your scenario
CellTemplate = Resources["FuzzyInnerTableTemplate"] as DataTemplate
Remove the UserControl.DataContext from FuzzyInnerTableControl, because otherwise the binding will fail miserably and you still get the default values. Find some other approach if you need to combine default and outside-provided values.
Confused? Well, so was I, so here's the summary of my testing code, including some made-up rows and columns, since I don't have your whole Column.Data and Table.Attributes stuff available:
XAML for FuzzyTable (Control, ViewModel)
<UserControl x:Class="WpfApplication2.FuzzyTableControl"
...
>
<UserControl.DataContext>
<viewModel:FuzzyTableViewModel/>
</UserControl.DataContext>
<UserControl.Resources>
<DataTemplate x:Key="FuzzyInnerTableTemplate">
<view:FuzzyInnerTableControl
DataContext="{Binding Tag,RelativeSource={RelativeSource AncestorType=DataGridCell}}"/>
</DataTemplate>
</UserControl.Resources>
<DataGrid AutoGenerateColumns="True" IsReadOnly="True"
CanUserAddRows="False"
CanUserDeleteRows="False"
ItemsSource="{Binding DataTable}"
AutoGeneratingColumn="DataGrid_AutoGeneratingColumn">
</DataGrid>
</UserControl>
Code for FuzzyTable
public partial class FuzzyTableControl : UserControl
{
public FuzzyTableControl()
{
InitializeComponent();
}
private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
e.Column = new DataGridTemplateColumn
{
CellStyle = new Style(typeof(DataGridCell))
{
Setters =
{
new Setter(DataGridCell.TagProperty, new Binding(e.PropertyName)),
}
},
Header = e.Column.Header,
HeaderStringFormat = e.Column.HeaderStringFormat,
HeaderStyle = e.Column.HeaderStyle,
HeaderTemplate = e.Column.HeaderTemplate,
HeaderTemplateSelector = e.Column.HeaderTemplateSelector,
// transfer whatever properties you feel worthy in your scenario
CellTemplate = Resources["FuzzyInnerTableTemplate"] as DataTemplate
};
}
}
public class FuzzyTableViewModel : BaseViewModel
{
public DataTable DataTable { get; set; }
public FuzzyTableViewModel()
{
DataTable = new DataTable();
DataTable.Columns.Add(new DataColumn("O", typeof(FuzzyInnerTableViewModel)));
DataTable.Columns.Add(new DataColumn("P", typeof(FuzzyInnerTableViewModel)));
var c1 = new FuzzyInnerTableViewModel();
var c2 = new FuzzyInnerTableViewModel();
c1.DataTable.Rows[1][0] = "Replace";
var row = new List<object>();
row.Add(c1);
row.Add(c2);
DataTable.Rows.Add(row.ToArray());
}
}
XAML for FuzzyInnerTable
<UserControl x:Class="WpfApplication2.FuzzyInnerTableControl"
...
Loaded="UserControl_Loaded">
<UserControl.Resources>
<viewModel:FuzzyInnerTableViewModel x:Key="defaultVM"/>
</UserControl.Resources>
<DataGrid ItemsSource="{Binding DataTable}"/>
</UserControl>
Code for FuzzyInnerTable
public partial class FuzzyInnerTableControl : UserControl
{
public FuzzyInnerTableControl()
{
InitializeComponent();
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
if (DataContext == null)
{
DataContext = Resources["defaultVM"];
}
}
}
public class FuzzyInnerTableViewModel : BaseViewModel
{
public DataTable DataTable { get; private set; }
public FuzzyInnerTableViewModel()
{
DataTable = new DataTable();
DataTable.Columns.Add(new DataColumn("A"));
DataTable.Columns.Add(new DataColumn("B"));
for (int rowId = 0; rowId < 3; rowId++)
{
var row = new List<object>();
row.Add(rowId);
row.Add(2 * rowId);
DataTable.Rows.Add(row.ToArray());
}
}
}
I have what I believe to be a potentially unique situation.
My ListBox items consist of the following:
StackPanel
Image
ListItem
The ListItem and Image are inserted into the StackPanel, then the StackPanel is the inserted into the ListBox for each item in the array.
Now the challenging part comes in sorting the content by the ListItem's Content (text) as it's a child of the StackPanel. Naturally, the StackPanel does not contain a Content member, so using the below code fails.
this.Items.SortDescriptions.Add(new System.ComponentModel.SortDescription("Content",
System.ComponentModel.ListSortDirection.Ascending));
So I figured, what if I set my StackPanel's data context to my ListItem, then surely it will find it.
stackPanel.DataContext = this.Items;
However, that also fails.
I'm creating my ListItems programatically in the code behind, via data that is loaded in via Json.Net.
My goal here is to sort the items from A-Z, based on the Items Content. I would prefer to keep my current implementation (creating the data programatically) as it gives me more control over the visuals. Plus, it's only about 20 lines of code.
Is it possible to use SortDescriptions when the ListItem's content is a StackPanel ?
Thank you
PS: Only started with WPF today, but have been developing WinForms apps for nearly 2 months.
The WPF way to do it would be to bind your ListBox ItemsSource to an ObservableCollection containing your items.
You would then be able to sort your observableCollection liks so :
CollectionViewSource.GetDefaultView(YourObservableCollection).SortDescriptions.Add(new SortDescription("PropertyToSort", ListSortDirection.Ascending));
Here is a small project that highlights this :
XAML :
<Window x:Class="stackPanelTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:stackPanelTest"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Label Content="{Binding Image}" />
<TextBlock Text="{Binding Item.Content}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
Code Behind :
public partial class MainWindow : Window
{
public ViewModel Items { get; set; } = new ViewModel();
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
}
ViewModel :
public class ViewModel : ObservableCollection<ListItem>
{
public ViewModel()
{
populateItems();
CollectionViewSource.GetDefaultView(this).SortDescriptions.Add(new SortDescription("Item.Content", ListSortDirection.Ascending));
}
private void populateItems()
{
addOneItem(0, "zero");
addOneItem(1, "one");
addOneItem(2, "two");
addOneItem(3, "three");
addOneItem(4, "four");
}
private void addOneItem(int img, string content)
{
ListItem item = new ListItem();
item.Image = img;
item.Item = new SomeItem { Content = content };
Add(item);
}
}
public class ListItem
{
public int Image { get; set; }
public SomeItem Item { get; set; }
}
public class SomeItem
{
public string Content { get; set; }
}
I took the liberty of renaming your "ListItem" into a "SomeItem" class because I didn't know what it was.
Then I made a "ListItem" class which is used to contain a Image/SomeItem pair (which is what your ListBox is composed of).
Also I used an int instead of an actual image but that should be easily changable.
Here's a screenshot of what I get when executing this code :
Hope this helps, good luck.
PS : if your items values are susceptible to change, don't forget to implement INotifyPropertyChanged in "SomeItem" and "ListItem", otherwise the change won't be updated in your view.
Is it possible to use SortDescriptions when the ListItem's content is a StackPanel ?
No. You will have to implement the sorting logic yourself.
There is no easy way to apply custom sorting to the ItemCollection that is returned from the Items property of the ListBox so instead of adding items to this one you could add the items to a List<StackPanel> and sort this one.
You could still create the data programatically just as before.
Here is an example for you:
Code:
public partial class MainWindow : Window
{
private List<StackPanel> _theItems = new List<StackPanel>();
public MainWindow()
{
InitializeComponent();
//create the items:
StackPanel sp1 = new StackPanel();
ListBoxItem lbi1 = new ListBoxItem() { Content = "b" };
Image img1 = new Image();
sp1.Children.Add(lbi1);
sp1.Children.Add(img1);
_theItems.Add(sp1);
StackPanel sp2 = new StackPanel();
ListBoxItem lbi2 = new ListBoxItem() { Content = "a" };
Image img2 = new Image();
sp2.Children.Add(lbi2);
sp2.Children.Add(img2);
_theItems.Add(sp2);
StackPanel sp3 = new StackPanel();
ListBoxItem lbi3 = new ListBoxItem() { Content = "c" };
Image img3 = new Image();
sp3.Children.Add(lbi3);
sp3.Children.Add(img3);
_theItems.Add(sp3);
//sort the items by the Content property of the ListBoxItem
lb.ItemsSource = _theItems.OrderBy(x => x.Children.OfType<ListBoxItem>().FirstOrDefault().Content.ToString()).ToList();
}
}
XAML:
<ListBox x:Name="lb" />
I am trying to show up a list of strings in a datagrid, but I haven't been able to make it work correctly. The xaml is:
<DataGrid x:Name="ListGrid" ItemsSource = "{Binding}" AutoGenerateColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Title" Binding="{Binding trackTitle}"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
The Welcome.xaml.cs looks something like this, where trackTitle is a List
public Welcome()
{
InitializeComponent();
ListGrid.DataContext = MainWindow.trackTitle;
}
When I preview it, I can see the correct number of rows but no data for Title column. Also there is a column generated for Length and it shows the correct length for each string. What am I doing wrong? Is the Binding parameter for the Title column {Binding trackTitle} not right?
Look at this example:
View
<Window x:Class="Example.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<DataGrid ItemsSource="{Binding Entries}"/>
</StackPanel>
</Window>
View Code-Behind
namespace Example
{
using System.Windows;
public partial class MainWindow : Window
{
public MainWindow()
{
DataContext = new MainWindowViewModel();
InitializeComponent();
}
}
}
ViewModel
namespace Example
{
using System.Collections.ObjectModel;
using System.Windows.Controls;
class MainWindowViewModel
{
public ObservableCollection<string> Entries { get; set; }
public MainWindowViewModel()
{
List<string> list = new List<string>() { "Entry 1", "Entry 2", "Entry 3" };
Entries = new ObservableCollection<string>(list);
}
}
}
Shows a good example of how to bind to a collection property in a ViewModel. Try applying this to your situation.
Good luck!