Custom comparer for the SortDescriptions in the ICollectionView - c#

I have the next problem.
There is some WPF dialog with DataGrid. This DataGrid displays some table. Table contains 1 column with a Date. This Date property is represented as a string. In most cases, Date has some value that we read from the database. But in some cases user can create a new record in this table. In this case, the Date should be null (with a "null" value, not a String.Empty). And it should be written to the database as null. Also the cell in the table for "null" Date should be empty. So it's why we use for it a string type.
The DataGrid ItemSource property has binding to the ICollectionView MyView property from the view model. Also, the view model contains other property related to my collection of data and is used for the MyView:
public class MyCustomObject
{
public string Date { get; set; }
// some other properties.
}
public class MyViewModel
{
public ObservableCollection<MyCustomObject> TableItems
{
get { return tableItems; }
set
{
Set(ref tableItems, value, true);
TableView = CollectionViewSource.GetDefaultView(tableItems);
}
}
public virtual ICollectionView TableView
{
get { return tableView; }
set
{
Set(ref tableView, value);
}
}
public MyViewModel()
{
var someCollection = new List<MyCustomObject>();
TableItems = new ObservableCollection<MyCustomObject>(someCollection);
}
}
In some method of MyViewModel I've added the sorting for my collection by ICollectionView.SortDescriptions. Like:
TableView.SortDescriptions.Add(new SortDescription(nameof(MyCustomObject.Date), ListSortDirection.Ascending));
All works. But as my Date is a string it's compared as a string. So I can get something like this:
02/10/19
03/09/19
03/10/20
04/08/17
How can I add some custom comparer to my TableView and change the sorting logic for some property?
Should I have to add the custom comparer to the my someCollection in the constructor before adding it to the TableItems?

You must cast the ICollectionView to ListCollectionView.
Depending on the actual collection, the view returned by CollectionViewSource.GetDefaultView is an implementation of ICollectionView.
For collections of type IList this is the ListCollectionView.
Also consider to use a DateTime instead of string to store the date.
The following example creates a date string IComparer or an alternate DateTime IComparer and assigns it to the collection's view:
var listCollectionView = (ListCollectionView)CollectionViewSource.GetDefaultView(this.TableItems);
// To compare DateTime
Comparer<DateTime> dateTimeComparer = Comparer<DateTime>.Create(DateTime.Compare);
// To compare date strings.
// Optionally define the date culture for DateTime.Parse
CultureInfo dateCulture = CultureInfo.CurrentCulture;
Comparison<string> dateTimeStringComparison = (stringX, stringY)
=> DateTime.Compare(DateTime.Parse(stringX, dateCulture), DateTime.Parse(stringY, dateCulture));
Comparer<string> dateTimeStringComparer = Comparer<string>.Create(dateTimeStringComparison);
listCollectionView.CustomSort = dateTimeStringComparer;

Related

How to one way bind the selected text value of a ComboBox to a property of an object

I have a combobox that's set to DropDownStyle=DropDownList (meaning users can't type anything, just select from dropdown). The combo contains a list of states.
I am trying to bind the selected text value to _model.StateBar, but my code doesn't seem to update the property of the object.
I've tried both of the following:
cboStates.DataBindings.Add("Text", _model, "StateBar")
cboStates.DataBindings.Add("SelectedItem", _model, "StateBar")
cboStates.DataBindings.Add("SelectedValue", _model, "StateBar")
I just need to bind it one way: updates from the control need to end up on the object.
Binding to ComboBox.SelectedValue should work, but only in case when you add items through ComboBox.DataSource.
public class Model
{
public string StateBar { get; set; }
}
// In the form
var states = new List<string> { "Alabama", "California" };
combobox.DataSource = states;
combobox.DataBindings.Add("SelectedValue", _model, "StateBar", true, DataSourceUpdateMode.OnPropertyChanged);
Binding to SelectedItem should work in all cases.

C# datagridview set databound column value to a chosen default one

I have a DataGridView bound to a list of custom objects. A column is bound to a double datatype, and when I try to empty its content (to null), it throws a data error indicating DbNull cannot be converted into Double. What I'd like to accomplish is when a user enters a null value for the databound column, I'd like to set it to a default generic value, say 3.0.
I can handle dataerror and replace the value there, but this seems like a hackish solution. What is the recommended way of handling this?
Edit:
Here's my data class. The double datatype mentioned above is USL.
public class SPCModelDTO
{
public string ProcessName { get; set; }
public string CustomerName { get; set; }
public string ModelName { get; set; }
public double USL { get; set; }
}
Here's my code for databinding
dgvModel.DataSource = new BindingList<SPCModelDTO>(modelList);
Matching property name is declared in designer w/ respect to the dataclass properties.
As a side note, I cannot change USL's datatype to a nullable double.
Create a ViewModel which will be bound to the DataGrdiView.
In the ViewModel you can wrap USL in the nullable type
public class SPCModelDTOViewModel: INotifyPropertyChanged
{
private SPCModelDTO _Model;
public double? USL
{
get { return _Model.USL;}
set
{
if(value.HasValue == false)
_Model.USL = 3.0;
else
_Model.USL = value;
this.RaisePropertyChangedEvent();
}
}
//Other model's properties
}
Line this.RaisePropertyChangedEvent(); will notify View(DataGridView) about changes and empty(null) value will be changed to default one
Of course your ViewModel class must implement INotifyPropertyChanged
How to: Implement the INotifyPropertyChanged Interface
for a databound column you can set DataSourceNullValue in DatagridViewColumn.DefaultCellStyle
dgvModel.DataSource = data;
var col = dgvModel.Columns["USL"];
col.DefaultCellStyle.DataSourceNullValue = 3;

Dynamic N columns bind to DetailsView?

I have a stored procedure that return the data below:
HEIGHT LENGTH WEIGHT WIDTH
0 0 0 0
The columns will be dynamic, can have up to N columns.
What I want to do is bind this result to Details View then let user key in the value.
But I have the error below when I bind to Details View:
DetailsView with id 'detailsViewProcessParameter' did not have any properties or attributes from which to generate fields. Ensure that your data source has content.
Here is the method that I use to return list of object then bind to details view:
public static List<object> GetProcessParameters(int formulaId)
{
using (var db = new SurfaceTreatment.SurfaceTreatmentEntities())
{
var paramFormulaId = new SqlParameter { ParameterName = "formulaId", Value = formulaId };
var query = db.Database.SqlQuery<object>("exec usp_GetProcessParameters #formulaId", paramFormulaId).ToList();
return query.ToList();
}
}
I suspect that I should use "select new { PropertyName = Value, PropertyName = Value}" in my LINQ in order to get rid of the error.
But how do I select new when PropertyName(column name) are dynamic?
How do I use reflection to solve this?
Or is there a better way to do this?
The problem here is that you are returning and thus binding a list of objects i.e. List<object> to DetailsView. I suspect that your object doesn't have any public property which DetailsView needs to Generate Rows and thus throws the error.
When using a list to bind to DetailsView ( or say a GridView ), the object in context, which is represented by some Class, MUST have public properties . For example suppose we bind a List of User List<User> to DetailsView, then here the User class MUST have public properties which will form the Rows for DetailsView ( or Columns in GridView).
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Mail { get; set; }
}
With this basic concept, check to see what does your query variable looks like and try to map this query variable to a Class with public properties.
Since in your case Columns are dynamic in nature, You must be first able to determine the total number of columns with there values, construct a corresponding DataTable at runtime and bind that data table to Detailsview.
If you set AutoGenerateRows to true for the control, all the rows should generate automatically. I do not see any need to perform reflection.
Note that any property which is set for DataKeyNames will not be editable.

Binding Custom List Property To DatagridView

I have a problem that is difficult to explain. Essentially I have a list of a certain class we can call MyObj. One of the properties of this object is a custom list itself. I would like to bind this List to a dataGridView and have this particular property that is also a list show up. Any ideas? Am I being clear enough? :-P..
Here is the idea. I have my own custom list object overriding the ToString() method:
public class CategoriesList : List<Category>
{
public override string ToString()
{...}
}
This is used as a property in an object such as:
public MyObj
{
public string Property1 {get; set; }
public string Property2 {get; set; }
public CategoriesList Categories {get; set; }
}
In turn, I have a list of these objects such as:
List<MyObj> myDataSouce = SomeRepository.GetMyObjList();
Where I bind this to a datagrid view:
MyDataGridView.DataSource = myDataSource;
Property1 and Property2 are automatically generated. Is there any way to have the CategoriesList property be added as well? I previously thought Overriding the ToString() method on a class would be enough..
I am really lost on this one as I have no idea how to even google for it :-P
Assuming that you'd like to display a specific value in place of the list in the datagridview, you'll want to use a custom TypeConverter. Otherwise you'll need to place a control in the datagridview column that supports lists, like a drop down list and bind to that.
For the former:
Basically decorate your categories property with a custom typeconverter:
[TypeConverter(typeof(MyConverter))]
public CategoriesList Categories { get; set; }
Then use a custom type converter that basically tells the datagrid that when it encounters the categories property what do display:
public class MyConverter : TypeConverter
{
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is CategoriesList) {
return value.ToString();
}
return base.ConvertFrom(context, culture, value);
}
}
You'll need to add your column to be databound manually by adding an unbound column and specify the DataPropertyName for the property to be mapped to that column, in this case "Categories"
If you're looking to display second level properties as well then this may help:
http://blogs.msdn.com/b/msdnts/archive/2007/01/19/how-to-bind-a-datagridview-column-to-a-second-level-property-of-a-data-source.aspx
This might help... look at my answer there, I haven't tried it with a property that is also a type of list but I think the idea is the same.
Or this one as well, I also have an answer there with a sample code too...

Howto determine order of displayed columns in a datagridview binded to a datasource

Is there a way to determine the order of the columns displayed in
a datagridview when binding it to a datasource whitch contains an
underlying IList ?
I thought there was a specific property attribute for this purpose
but can't recall what it actually was.
eg:
public void BindToGrid(IList<CustomClass> list)
{
_bindingSource.DataSource = list;
dataGridView1.DataSource = _bindingSource.DataSource;
}
Type binded should be something like this
class CustomClass
{
bool _selected = false;
//[DisplayOrder(0)]
public bool Selected
{
get { return _selected; }
set { _selected = value; }
}
string _name;
//[DisplayOrder(2)]
public string Name
{
get { return _name; }
set { _name = value; }
}
string _value;
//[DisplayOrder(1)]
public string Value
{
get { return _value; }
set { _value = value; }
}
}
Edit:
I would like to add that I rather not want to add the columns manually to columns list in the designer. I'd like to keep this as dynamic as possible.
In the DataGridView specify an actual list of columns instead of allowing it to auto-databind. You can do this in Design View in Visual Studio by selecting the control and adding the columns. Make sure you specify in each column which property it should bind to. Then you can rearrange the columns any way you want as well as do other customizations.
I think that the DisplayOrder attribute is relatively new and probably not supported in the DataGridView control.
The display order of the columns in the DataGridView is determined by the DisplayIndex properties of the DataGridViewColumn-s. You would have to set these properties on the columns of the grid, in order to change their order.
I also agree with Eilon's answer: you can create the list of the columns yourself, instead of auto-databinding, and that way you can determine the order in which they will be displayed.
The column ordering does not always work. You'll need to turn off AutoColumnCreate to fix inconsistencies:
http://www.internetworkconsulting.net/content/datadridview-displayorder-not-working
I am not sure whether this is a functionality that .Net Offers, but if you just change the order of your properties in the class, the grid renders the columns in the same order.
The below two classes will render in the order they are typed in the class. Strange!!
class CustomClass
{
public bool Selected {get;set;}
public string Name{get;set;}
}
class CustomClass
{
public string Name{get;set;}
public bool Selected {get;set;}
}

Categories