WPF DataGrid conditional RowHeader problem - wpf

I want to conditionally show different RowHeader contents in the WPF datagrid. E.g. My Model has two properties, Name and IsSuperHero. If the row represents a super-hero I want to show textbox with 'SH' in the row-header.
I am trying to achieve this using datatriggers as below. The problem is, the text-box is shown only on one row (the last matching row).
Below, I expect 'SH' to be shown against Superman and Batman but it shows up only against Batman. If I sort on Name column the records are sorted to 'Batman, John, Peter, Superman' and now the 'SH' header is shown against 'Superman' (that happens to be last matching record).
Am I missing something here?
public class UserModel
{
public String Name { get; set; }
public Boolean IsSuperHero { get; set; }
public UserModel (String name, Boolean issuperhero) { Name = name; IsSuperHero = issuperhero; }
}
public partial class MainWindow : Window
{
private void Window_Loaded (object sender, RoutedEventArgs e){
List<UserModel> list = new List<UserModel>();
list.Add(new UserModel("Peter", false));
list.Add(new UserModel("Superman", true));
list.Add(new UserModel("John", false));
list.Add(new UserModel("Batman", true));
myDataGrid.ItemsSource = list;
//Show Header 'SH' for Super Hero
TextBox txtBox = new TextBox();
txtBox.Text = "SH";
Style rowStyle = new Style();
DataTrigger dataTrigger = new DataTrigger();
dataTrigger.Binding = new Binding("IsSuperHero");
dataTrigger.Value = true;
Setter setter = new Setter(DataGridRow.HeaderProperty, txtBox);
dataTrigger.Setters.Add(setter);
rowStyle.Triggers.Add(dataTrigger);
myDataGrid.RowStyle = rowStyle;
}

The TextBox is only created once. You can add the TextBox as a resource in some dictionary and set x:IsShared to false, then reference it using the StaticResourceMarkupExtension, that way a new one will be created in every call.
(As x:Shared cannot be set in code this approach should be taken to XAML, i do not see any reason why one would want to do this in code in the first place)

Related

Binding ListView's GridView to List<KeyValuePair>

Here's how I am making my GridView:
The ListView will contain Entry objects which looks like this:
public class Entry
{
public Entry(BitmapImage icon = null, List<EntryKeyValuePair> entryKeyValuePairs = null)
{
Icon = icon;
EntryKeyValuePairs = entryKeyValuePairs ?? new List<EntryKeyValuePair>();
}
public BitmapImage Icon { get; set; }
public List<EntryKeyValuePair> EntryKeyValuePairs { get; }
}
EntryKeyValuePair is just a KeyValuePair<string,string> where Key is the Column and Value is the value of the column. I used a List of KeyValuePair because I want to preserve insertion order. Anyway, here's how I am constructing the GridView.
GridView = new GridView();
foreach (Column column in Category.Columns.Where(c => c.IsVisibleInTable)) {
var gridViewColumn = new GridViewColumn {
Header = column.Name,
DisplayMemberBinding = new Binding($"EntryKeyValuePairs[{column.Name}].Value")
};
GridView.Columns.Add(gridViewColumn);
}
I don't know what binding to set in DisplayMemberBinding. The above binding would work if EntryKeyValuePairs was a dictionary. But in my case it is not.
If I had access to the data object somehow, I could do
DisplayMemberBinding = new Binding($"EntryKeyValuePairs[{entry.EntryKeyValuePairs.FindIndex(p => p.Key == column.Name)}].Value")
How can I access the current Data Object which the ListView is holding while binding?
I found a solution. I used the GridViewColumn's CellTemplateSelector so that I can get a reference to the ListViews bound object. Here is how the CellTemplateSelector looks like. I had to create the DataTemplates in code.
class GridViewCellTemplateSelector : DataTemplateSelector
{
private readonly string _columnName;
public GridViewCellTemplateSelector(string columnName)
{
_columnName = columnName;
}
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var entry = (Entry)item;
var dataTemplate = new DataTemplate {
DataType = typeof (Entry)
};
var stackPanelFactory = new FrameworkElementFactory(typeof(StackPanel));
stackPanelFactory.SetValue(StackPanel.OrientationProperty, Orientation.Vertical);
var text = new FrameworkElementFactory(typeof(TextBlock));
text.SetBinding(TextBlock.TextProperty, new Binding($"EntryKeyValuePairs[{entry.EntryKeyValuePairs.FindIndex(p => p.Key == _columnName)}].Value"));
stackPanelFactory.AppendChild(text);
dataTemplate.VisualTree = stackPanelFactory;
return dataTemplate;
}
}
Instead of DisplayMemberBinding, I used this TemplateSelector:
CellTemplateSelector = new GridViewCellTemplateSelector(column.Name)
All good. Hope this helps someone :) I still hope to see a better solution than this.

How to edit elements in a collection from another window or Dialog

I have a number of collection bound to the application main window controls shown in a
simplified form below. There are a number of other elements in the view model
(Ommited for Clarity) which all update and work as expected.
I require to edit a collection's element in another window, with the edited data back in the origonal collection.
/// Example of the Collection and Properties
ObservableCollection<MyData> _MyCollection = new ObservableCollection<MyData>();
public ObservableCollection<MyData> MyCollection { get { return _MyCollection; } }
public class MyData : INotifyPropertyChanged
{
private bool cb_checked;
public string Param1 { get; set; }
public string Param2 { get; set; }
public bool myCheck
{
get { return cb_checked; }
set
{
if (cb_checked == value) return;
cb_checked = value;
RaisePropertyChanged("Checked");
}
}
}
My problem is how do I pass an item of a collection to a new window for editing.
My intal thoughts were to pass the item in the constructor of the window
Dialog.Edit window = new Dialog.Edit(_MyCollection[2] );
window.Owner = this;
window.Show();
I also tried this as I have read I cant use indexed references
var tmp = _MyCollection[2];
Dialog.Edit window = new Dialog.Edit( tmp);
window.Owner = this;
window.Show();
but this does not work and I get null exceptions whe trying to access elements.
If I need to pass the complete collection this is also ok as they are all quite small i.e. < 50 items.
I must be going about this in the wrong way, could someone please explain how to do this
correctly please.
Many Thanks
Sarah

Silverlight ComboBox Filter - Show All

In Silverlight5 with RIA Services, using DomainDataSources.
I have a filter ComboBox which a DataGrid is bound to.
Problem: How to implement a "Select All" at the top of the combo box - have currently done it by putting a row of ID=0 in the database and using IgnoredValue="0" in the filter
<riaControls:DomainDataSource.FilterDescriptors>
<riaControls:FilterDescriptor PropertyPath="AccountTypeID" Operator="IsEqualTo" IgnoredValue="0" Value="{Binding Path=SelectedItem.AccountTypeID,
ElementName=AccountTypeComboBox, FallbackValue=0}" />
</riaControls:DomainDataSource.FilterDescriptors>
</riaControls:DomainDataSource>
<riaControls:DomainDataSource x:Name="AccountTypeDataSource" AutoLoad="True" QueryName="GetAccountTypes" PageSize="15" LoadSize="30">
<riaControls:DomainDataSource.DomainContext>
<domain:xxxDomainContext />
</riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>
Would like to manually add the Show All in code after the data has been loaded in the ComboBox
EDIT
Thanks to Martin below I got it working like this:
private xxxDomainContext xxxDomainContext;
public MainPage()
{
InitializeComponent();
// Load up the AccountType ComboBox - instead of doing it in XAML
// then add in the Select All via proxy class above
xxxDomainContext = new xxxDomainContext();
EntityQuery<AccountType> query = xxxDomainContext.GetAccountTypesQuery();
LoadOperation loadOperation = xxxDomainContext.Load<AccountType>(query);
// everything is async so need a callback, otherwise will get an empty collection when trying to iterate over it here
loadOperation.Completed += AccountTypeLoadOperationCompleted;
and
private void AccountTypeLoadOperationCompleted(object sender, System.EventArgs e)
{
// create new proxy class
var listOfSelectableObjects = new List<SelectableObject<int>>();
var selectAll = new SelectableObject<int> { Display = "Select All", KeyValue = 0};
listOfSelectableObjects.Add(selectAll);
// load values into new list
foreach (var accountType in xxxDomainContext.AccountTypes)
{
var so = new SelectableObject<int>();
so.Display = accountType.Description;
so.KeyValue = accountType.AccountTypeID;
listOfSelectableObjects.Add(so);
}
AccountTypeComboBox.ItemsSource = listOfSelectableObjects;
// Set index to 0 otherwise a blank item will appear at the top and be selected
AccountTypeComboBox.SelectedIndex = 0;
}
In the past, I have achieved this by creating a proxy class, something like SelectableValue with a display, key and isDefault as shown below:-
public class SelectableObject<K>
{
public string Display { get; set; }
public K KeyValue { get;set; }
public bool IsDefaultSelection { get; set; }
}
This means that I can then transform the list of items I am selecting from into a List<SelectableObject<int>> for example, and optionally add an extra item to that list that is Display="Select All" and IsDefault=true rather than trying to bind directly to the list.
Hope that helps.

DataGrid 'EditItem' is not allowed for this view when dragging multiple items

I have a datagrid which gets data like this:
public struct MyData
{
public string name { set; get; }
public string artist { set; get; }
public string location { set; get; }
}
DataGridTextColumn col1 = new DataGridTextColumn();
col4.Binding = new Binding("name");
dataGrid1.Columns.Add(col1);
dataGrid1.Items.Add((new MyData() { name = "Song1", artist = "MyName", location = "loc"}));
dataGrid1.Items.Add((new MyData() { name = "Song2", artist = "MyName", location = "loc2"}));
The problem is- whenever a user tries to edit a cell or drags multiple cells- the app throws an exception:
System.InvalidOperationException was unhandled
Message: 'EditItem' is not allowed for this view.
Why is this? Is it because of the way the data is entered?
Any ideas?
Thanks!
I got this issue when assigning ItemsSource to IEnumerable<T>.
I fixed it by converting the IEnumberable<T> to a List<T> and then assigning that to ItemsSource.
I'm not sure why using IEnumerable caused that issue, but this change fixed it for me.
Instead of using a struct use a class instead.
UPDATED ANSWER: Try adding your MyData instances to a List then assigning that list to the DataGrid.ItemsSource
If you use datagrid DataGridCheckBoxColumn you need to set <Setter Property="IsEditing" Value="true" />
on check box column. See this: https://stackoverflow.com/a/12244451/1643201
This answer is not my own, just the working code example suggested by AnthonyWJones.
public class MyData //Use class instead of struct
{
public string name { set; get; }
public string artist { set; get; }
public string location { set; get; }
}
DataGridTextColumn col1 = new DataGridTextColumn();
col4.Binding = new Binding("name");
dataGrid1.Columns.Add(col1);
dataGrid1.Items.Add((new MyData() { name = "Song1", artist = "MyName", location = "loc"}));
dataGrid1.Items.Add((new MyData() { name = "Song2", artist = "MyName", location = "loc2"}));
//Create a list of MyData instances
List<MyData> myDataItems = new List<MyData>();
myDataItems.Add(new MyData() { name = "Song1", artist = "MyName", location = "loc"});
myDataItems.Add(new MyData() { name = "Song2", artist = "MyName", location = "loc2"});
//Assign the list to the datagrid's ItemsSource
dataGrid1.ItemsSource = items;
For my case,
processLimits.OrderBy(c => c.Parameter);
returns an
IOrderedEnumerable<ProcessLimits>
not a
List<ProcessLimits>
so when I assign a style for my event setter to a checkbox column in my datagrid
style.Setters.Add(new EventSetter(System.Windows.Controls.Primitives.ToggleButton.CheckedEvent, new RoutedEventHandler(ServiceActiveChecked)));
ServiceActiveChecked is never called and I got
'EditItem' is not allowed for this view.
and for anyone else doing checkboxes in datagrid columns, I use a column object with my column data in this constructor for adding the data grid I use with adding the style above.
datagridName.Columns.Add(new DataGridCheckBoxColumn()
{
Header = column.HeaderText.Trim(),
Binding = new System.Windows.Data.Binding(column.BindingDataName.Trim()) { StringFormat = column.StringFormat != null ? column.StringFormat.Trim().ToString() : "" },
IsReadOnly = column.IsReadOnlyColumn,
Width = new DataGridLength(column.DataGridWidth, DataGridLengthUnitType.Star),
CellStyle = style,
});
I solved this by setting the datagrid's source after the InitializeComponent:
public MainWindow()
{
InitializeComponent();
FilterGrid.ItemsSource = ScrapeFilter;
}

winForms + DataGridView binding to a List<T>

I'm trying to bind a List<T> to a DataGridView control, and I'm not having any luck creating custom bindings.
I have tried:
gvProgramCode.DataBindings.Add(new Binding("Opcode",code,"Opcode"));
It throws an exception, saying that nothing was found by that property name.
The name of the column in question is "Opcode". The name of the property in the List<T> is Opcode.
ANSWER EDIT: the problem was that I did not have the bindable fields in my class as properties, just public fields...Apparently it doesn't reflect on fields, just properties.
Is the property on the grid you are binding to Opcode as well?.. if you want to bind directly to List you would just DataSource = list. The databindings allows custom binding. are you trying to do something other than the datasource?
You are getting a bunch of empty rows? do the auto generated columns have names? Have you verified data is in the object (not just string.empty) ?
class MyObject
{
public string Something { get; set; }
public string Text { get; set; }
public string Other { get; set; }
}
public Form1()
{
InitializeComponent();
List<MyObject> myList = new List<MyObject>();
for (int i = 0; i < 200; i++)
{
string num = i.ToString();
myList.Add(new MyObject { Something = "Something " + num , Text = "Some Row " + num , Other = "Other " + num });
}
dataGridView1.DataSource = myList;
}
this should work fine...
I can't really tell what you're trying to do with the example you included, but binding to a generic list of objects is fairly straightforward if you just want to list the objects:
private BindingSource _gridSource;
private BindingSource GridSource
{
get
{
if (_gridSource == null)
_gridSource = new BindingSource();
return _gridSource;
}
}
private void Form1_Load(object sender, EventArgs e)
{
List<FluffyBunny> list = new List<FluffyBunny>();
list.Add(new FluffyBunny { Color = "White", EarType = "Long", Name = "Stan" });
list.Add(new FluffyBunny { Color = "Brown", EarType = "Medium", Name = "Mike" });
list.Add(new FluffyBunny { Color = "Mottled", EarType = "Short", Name = "Torvald" });
GridSource.DataSource = list;
dataGridView1.Columns["EarType"].Visible = false; //Optionally hide a column
dataGridView1.DataSource = GridSource;
}
If you only want to display specific properties of the List's type you should be able to make the unwanted column(s) invisible.
Technically, you don't really need to create the BindingSource, but I find it's a whole lot easier when I'm doing updates or changes if I have it.
Hope this helps.
Had the same issue... I had a struct with public fields obviously. nothing in the grid. provided public getters, worked.
Another solution I've found is to use the BindingList collection.
private void Form1_Load(object sender, EventArgs e)
{
BindingList people= new BindingList {
new Person {Name="John",Age=23},
new Person {Name="Lucy",Age=16}
};
dataGridView1.DataSource= people;
}
It works fine for me,

Resources