I'm having hard times to show some simple labels in my Cartesian Bar Charts, I'm reading a lot around but nothing seems to work for me. I'm using the MVVM pattern in my project, so this is the code I have so far..
VIEW
<lvc:CartesianChart Grid.Row="2" Series="{Binding ChartDataSets}">
<lvc:CartesianChart.AxisX>
<lvc:Axis LabelsRotation="20" Labels="{Binding ColumnLabels}" Position="RightTop" >
<lvc:Axis.Separator >
<lvc:Separator Step="1"></lvc:Separator>
</lvc:Axis.Separator>
</lvc:Axis>
</lvc:CartesianChart.AxisX>
<lvc:CartesianChart.AxisY>
<lvc:Axis LabelFormatter="{Binding Formatter}" Position="RightTop"></lvc:Axis>
</lvc:CartesianChart.AxisY>
</lvc:CartesianChart>
DataModel
class DataModel : INotifyPropertyChanged
{
private double value;
public double Value
{
get => this.value;
set
{
this.value = value;
OnPropertyChanged();
}
}
private string label;
public string Label
{
get => this.label;
set
{
this.label = value;
OnPropertyChanged("Label");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
ViewModel
class BackupStatsViewModel : INotifyPropertyChanged
{
ChartValues<DataModel> values = new ChartValues<DataModel>();
public SeriesCollection ChartDataSets { get; set; }
public ObservableCollection<string> ColumnLabels { get; set; }
public class ErrorPrt
{
public ErrorPrt(){
prtName = string.Empty;
Count = -1;
}
public string prtName { get; set; }
public int Count { get; set; }
}
public BackupStatsViewModel()
{
InitializeBarChartData();
}
private void InitializeBarChartData()
{
this.ColumnLabels = new ObservableCollection<string>(values.Select(dataModel => dataModel.Label));
var dataMapper = new CartesianMapper<DataModel>()
.Y(dataModel => dataModel.Value)
.Fill(dataModel => dataModel.Value > 15.0 ? Brushes.Red : Brushes.Green);
this.ChartDataSets = new SeriesCollection
{
new ColumnSeries
{
Values = values,
Configuration = dataMapper,
DataLabels = true
}
};
}
public ErrorPrt[] PrtCount(List<DataRow> rows)
{
IEnumerable<IGrouping<string, DataRow>> grouped = rows.GroupBy(s => s.Field<string>(2));
ErrorPrt[] err = new ErrorPrt[grouped.Count()];
//Omitted code for sake of brevity
ErrorPrt[] arr = err.Where(c => c != null).ToArray();
for (int i = 0; i < arr.Count(); i++)
values.Add(new DataModel() { Label = $"PRT {arr[i].prtName}", Value = arr[i].Count });
return arr;
}
}
But as you can see no labels are shown on the X axis.. really don't know how to bypass this problem in order to go on with my work..please can anyone show me the right way?
Your flow looks broken:
you first initialize the chart data from the constructor by calling InitializeBarChartData(), which also initializes the ColumnLabels collection. Then you create the underlying ErrorPtr items, which are the provider of the data for the column labels.
The result is that the ColumnLabels property is empty => no labels will be displayed.
Because you add the new ErrorPtr items to the values field and this field is of type ChartValues and this collection implements INotifyCollectionChanged, the chart will reflect those changes. You were lucky here.
But because you never update the ColumnLabels property after you have created the ErrorPtr items, the initially (after calling InitializeBarChartData from the constructor) empty ColumnLabels collection remains empty.
Solution 1
Fix the flow of your data model initialization and call InitializeBarChartData after PrtCount:
public ErrorPrt[] PrtCount(List<DataRow> rows)
{
IEnumerable<IGrouping<string, DataRow>> grouped = rows.GroupBy(s => s.Field<string>(2));
ErrorPrt[] err = new ErrorPrt[grouped.Count()];
//Omitted code for sake of brevity
ErrorPrt[] arr = err.Where(c => c != null).ToArray();
for (int i = 0; i < arr.Count(); i++)
this.values.Add(new DataModel() { Label = $"PRT {arr[i].prtName}", Value = arr[i].Count });
// Initialize the chat models.
// NOW the label data (the ErrorPrt.prtName) is generated
// and ready to be extracted from the ErrorPrt instances
InitializeBarChartData();
return arr;
}
Solution 2 (Recommended)
Since all involved collections implement INotifyCollectionChanged you can dynamically update every collection when new data arrives. You don't need to initialize the complete chart data like the SeriesCollection and the Mapper or the label formatter over and over again (like in Solution 1 - in case PrtCount will be called more than once).
You can continue to call InitializeBarChartData once from the constructor, like you are currently doing it.
Simply don't only update the values field, but also the ColumnLabels property:
public ErrorPrt[] PrtCount(List<DataRow> rows)
{
IEnumerable<IGrouping<string, DataRow>> grouped = rows.GroupBy(s => s.Field<string>(2));
ErrorPrt[] err = new ErrorPrt[grouped.Count()];
//Omitted code for sake of brevity
ErrorPrt[] arr = err.Where(c => c != null).ToArray();
for (int i = 0; i < arr.Count(); i++)
{
var newDataModel = new DataModel() { Label = $"PRT {arr[i].prtName}", Value = arr[i].Count };
// Here you update the column values
// and add the new items to the existing items of previous calls
this.values.Add(newDataModel);
// Also update the labels whenever new column data has arrived
this.ColumnLabels.Add(newDataModel.Label);
}
return arr;
}
Related
Given the code
class Foo {
public string Value {get; set;}
public int Id {get; set;}
}
List<List<Foo>> fooList = new List<List<Foo>>();
Is there a way to bind a Multidim ICollection to a DataGridView on the property Value, where when you change a cell, the Value property of the object updates?
In this case, each instance of Foo in the list will represent one cell in the DataGridView and the rows/ columns are being preserved as they would be in the multidim ICollection
By Multidim I mean something to the affect of:
List<List<Foo>] => [
List<Foo> => [0,1,2,3,4,5]
List<Foo> => [0,1,2,3,4,5]
List<Foo> => [0,1,2,3,4,5]
List<Foo> => [0,1,2,3,4,5]
]
Where each element in the nested list is actually and instance of Foo.
Implementing IListSource and mapping to DataTabe internally
You can create a custom data source which implements IListSource and set it as data source of DataGridView. To implement the interface properly to satisfy your requirement:
In constructor, accept original list and map it to a DataTable.
Subscribe to ListChanged event of the DefaultView property of you data table and apply changes to your original list.
For GetList method, return the mapped data table.
Then when you bind DataGridView to your new data source, all the editing operations will immediately reflect in your original list:
dataGridView1.DataSource = new FooDataSource(yourListOfListOfFoo);
ListListDataSource Implementation
public class ListListDataSource<T> : IListSource
{
List<List<T>> data;
DataTable table;
public ListListDataSource(List<List<T>> list)
{
this.data = list;
table = new DataTable();
for (int i = 0; i < list.First().Count(); i++)
{
TypeDescriptor.GetProperties(typeof(T)).Cast<PropertyDescriptor>()
.Where(p => p.IsBrowsable).ToList().ForEach(p =>
{
if (p.IsBrowsable)
{
var c = new DataColumn($"[{i}].{p.Name}", p.PropertyType);
c.ReadOnly = p.IsReadOnly;
table.Columns.Add(c);
}
});
}
foreach (var innerList in list)
{
table.Rows.Add(innerList.SelectMany(
x => TypeDescriptor.GetProperties(typeof(T)).Cast<PropertyDescriptor>()
.Where(p => p.IsBrowsable).Select(p => p.GetValue(x))).ToArray());
}
table.DefaultView.AllowDelete = false;
table.DefaultView.AllowNew = false;
table.DefaultView.ListChanged += DefaultView_ListChanged;
}
public bool ContainsListCollection => false;
public IList GetList()
{
return table.DefaultView;
}
private void DefaultView_ListChanged(object sender, ListChangedEventArgs e)
{
if (e.ListChangedType != ListChangedType.ItemChanged)
throw new NotSupportedException();
var match = Regex.Match(e.PropertyDescriptor.Name, #"\[(\d+)\]\.(\w+)");
var index = int.Parse(match.Groups[1].Value);
var propertyName = match.Groups[2].Value;
typeof(T).GetProperty(propertyName).SetValue(data[e.NewIndex][index],
table.Rows[e.NewIndex][e.PropertyDescriptor.Name]);
}
}
Then bind your list to DataGridView like this:
List<List<Foo>> foos;
private void Form1_Load(object sender, EventArgs e)
{
foos = new List<List<Foo>>{
new List<Foo>(){
new Foo() { Id=11, Value="11"}, new Foo() { Id = 12, Value = "12" }
},
new List<Foo>() {
new Foo() { Id=21, Value="21"}, new Foo() { Id = 22, Value = "22" }
},
};
dataGridView1.DataSource = new ListListDataSource<Foo>(foos);
}
And when you edit data in DataGridView, in fact you are editing the original list.
Also if you want to hide a property, it's as easy as adding [Browsable(false)] to the property:
public class Foo
{
[Browsable(false)]
public int Id { get; set; }
public string Value { get; set; }
}
The problem you describe can be solved a couple of different ways. One is to “flatten” each List<Foo>. Basically this will flatten ALL the Foo items in a list into a “single” string. With this approach as I commented, you would end up with one column and each row would be a “flattened” List<Foo>. Each cell may have a different number of Foo items in the string.
In this case and as others, this may not be the desired result. Since you have a List of Lists, then a “Master-Detail" approach using two (2) grids may make things easier. In this approach, the first grid (master) would have one (1) column and each row would be a List<Foo>. Since we already know the grid will not display this LIST into a single cell AND we don’t want to “flatten” the list, then this is where the second (detail) grid comes into play. The first grid displays all the lists of Foo, and whichever “row” is selected, the second grid (detail) will display all the List<Foo> items.
An example may work best to show what I mean. First, we need to make an additional class. Reason being that is if we use a List<List<Foo>> as a DataSource to the master grid, it will show something like…
As shown the two columns are going to be the List “Capacity” and “Count.” This may work; however, it may be confusing to the user. That is why we want this other class. It is a simple “wrapper” around the List<Foo> and to display this we will add a “Name” property to this class. This will be displayed in the master grid.
Given the current modified Foo class…
public class Foo {
public string Value { get; set; }
public int Id { get; set; }
}
This FooList class may look something like…
public class FooList {
public string ListName { get; set; }
public List<Foo> TheFooList { get; set; }
}
A List<FooList> would display something like…
Now, when the user “selects” a row in the first “Master” grid, the second “Detail" grid will display all the Foo items in that list. A full example is below. Drop two grids onto a form and copy the code below to follow.
To help, a method that returns a List<Foo> where there are a random number of Foo items in each list. This method may look something like below with the global rand Random variable to get a random number of Foo items to add to the list in addition to setting a random Value for each Foo object.
Random rand = new Random();
private List<Foo> GettRandomNumberOfFooList() {
int numberOfFoo = rand.Next(2, 20);
List<Foo> fooList = new List<Foo>();
for (int i = 0; i < numberOfFoo; i++) {
fooList.Add(new Foo { Id = i, Value = rand.Next(1, 100).ToString() });
}
return fooList;
}
We can use this method to create a List<FooList> for testing. The master grids DataSource will be this list. Then, to determine which list to display in the details grid, we will simply use the selected FooList.TheFooList property.
Next, we need a trigger to know when to “change” the details data source. In this case I used the grids, RowEnter method to change the details grids data source.
Below is the code described above. The master grid will have 15 FooList items.
List<FooList> FooLists;
public Form1() {
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e) {
FooLists = new List<FooList>();
for (int i = 0; i < 15; i++) {
FooLists.Add(new FooList { ListName = "Foo List " + (i + 1), TheFooList = GettRandomNumberOfFooList() });
}
dataGridView1.DataSource = FooLists;
dataGridView2.DataSource = FooLists[0].TheFooList;
}
private void dataGridView1_RowEnter(object sender, DataGridViewCellEventArgs e) {
dataGridView2.DataSource = FooLists[e.RowIndex].TheFooList;
}
This should produce something like...
Lastly, this is just an example and using a BindingList/BindingSource may make things easier. This is a very simple example of using a “Master-Detail” approach with a List of Lists.
Using Custom TypeDescriptor
An interesting approach is creating a new data source using a custom TypeDescriptor.
Type descriptor provide information about type, including list of properties and getting and setting property values. DataTable also works the same way, to show list of columns in DataGridView, it returns a list of property descriptors containing properties per column.
Then when you bind DataGridView to your new data source, you are in fact editing the original list:
dataGridView1.DataSource = new FooDataSource(yourListOfListOfFoo);
ListListDataSource implementation using TypeDescriptor
Here I've created a custom type descriptor for each inner list to treat is as a single object having a few properties. The properties are all properties of each element of the inner list and I've created a property descriptor for properties:
public class ListListDataSource<T> : List<FlatList>
{
public ListListDataSource(List<List<T>> list)
{
this.AddRange(list.Select(x =>
new FlatList(x.Cast<object>().ToList(), typeof(T))));
}
}
public class FlatList : CustomTypeDescriptor
{
private List<object> data;
private Type type;
public FlatList(List<object> data, Type type)
{
this.data = data;
this.type = type;
}
public override PropertyDescriptorCollection GetProperties()
{
return this.GetProperties(new Attribute[] { });
}
public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
var properties = new List<PropertyDescriptor>();
for (int i = 0; i < data.Count; i++)
{
foreach (PropertyDescriptor p in TypeDescriptor.GetProperties(type))
properties.Add(new FlatListProperty(i, p));
}
return new PropertyDescriptorCollection(properties.ToArray());
}
public object this[int i]
{
get => data[i];
set => data[i] = value;
}
}
public class FlatListProperty : PropertyDescriptor
{
int index;
PropertyDescriptor originalProperty;
public FlatListProperty(int index, PropertyDescriptor originalProperty)
: base($"[{index}].{originalProperty.Name}",
originalProperty.Attributes.Cast<Attribute>().ToArray())
{
this.index = index;
this.originalProperty = originalProperty;
}
public override Type ComponentType => typeof(FlatList);
public override bool IsReadOnly => false;
public override Type PropertyType => originalProperty.PropertyType;
public override bool CanResetValue(object component) => false;
public override object GetValue(object component) =>
originalProperty.GetValue(((FlatList)component)[index]);
public override void ResetValue(object component) { }
public override void SetValue(object component, object value) =>
originalProperty.SetValue(((FlatList)component)[index], value);
public override bool ShouldSerializeValue(object component) => true;
}
To bind data:
List<List<Foo>> foos;
private void Form1_Load(object sender, EventArgs e)
{
foos = new List<List<Foo>>{
new List<Foo>(){
new Foo() { Id=11, Value="11"}, new Foo() { Id = 12, Value = "12" }
},
new List<Foo>() {
new Foo() { Id=21, Value="21"}, new Foo() { Id = 22, Value = "22" }
},
};
dataGridView1.DataSource = new ListListDataSource<Foo>(foos);
}
And when you edit data in DataGridView, in fact you are editing the original list.
Also if you want to hide a property, it's as easy as adding [Browsable(false)] to the property:
public class Foo
{
[Browsable(false)]
public int Id { get; set; }
public string Value { get; set; }
}
Flattening the List<List<T>> into a List<T>
If showing data in a flattened structure for editing is acceptable, then you can use:
List<List<Foo>> foos;
private void Form1_Load(object sender, EventArgs e)
{
foos = new List<List<Foo>>{
new List<Foo>(){
new Foo() { Id=11, Value="11"}, new Foo() { Id = 12, Value = "12" }
},
new List<Foo>() {
new Foo() { Id=21, Value="21"}, new Foo() { Id = 22, Value = "22" }
},
};
dataGridView1.DataSource = foos.SelectMany(x=>x).ToList();
}
And edit data in a flat list, like this:
When you edit each row, you are in fact editing the original list.
Am I missing something or is there more to it that I am not getting? I'm working on a mobile app and have to use pickers for choices from a data table. To start, I have many such pickers that are key/value based. I have an internal ID and a corresponding Show value. The IDs do not always have 1, 2, 3 values such as originating from a lookup table and may have things as
KeyID / ShowValue
27 = Another Thing
55 = Many More
12 = Some Item
Retrieved as simple as
select * from LookupTable where Category = 'demo'
So I have this class below that is used for binding the picker via a list of records
public class CboIntKeyValue
{
public int KeyID { get; set; } = 0;
public string ShowValue { get; set; } = "";
}
Now, the data record that I am trying to bind to has only the ID column associated to the lookup. Without getting buried into XAML, but in general, I have my ViewModel. On that I have an instance of my data record that has the ID column.
public class MyViewModel : BindableObject
{
public MyViewModel()
{
// Sample to pre-load list of records from data server of KVP
PickerChoices = GetDataFromServerForDemo( "select * from LookupTable where Category = 'demo'" );
ShowThisRecord = new MyDataRec();
// for grins, I am setting the value that SHOULD be defaulted
// in picker. In this case, ID = 12 = "Some Item" from above
ShowThisRecord.MyID = 12;
}
// this is the record that has the "ID" column I am trying to bind to
public MyDataRec ShowThisRecord {get; set;}
// The picker is bound to this list of possible choices
public List<CboIntKeyValue> PickerChoices {get; set;}
}
I can’t bind to the index of the list, because that would give me 0, 1, 2, when I would be expecting the corresponding "ID" to be the basis of proper record within the list.
In WPF, I have in the past, been able to declare the show value for the screen, but also the bind value to the ID column in similar. So, the binding of the INT property on my "ShowThisRecord" would drive and properly refresh.
I can see the binding of SelectedItem, but that is the whole item of the KVP class which is not part of the MyDataRec. Only the ID is the common element between them.
What is the proper bindings to get this to work?
<Picker ItemDisplayBinding="{Binding ShowValue}"
SelectedItem="{Binding ???}" />
Just to confirm my record bindings are legitimate, my page has binding context to MyViewModel as I can properly see the ID via a sample text entry I added to the page via.
<Entry Text="{Binding Path=ShowThisRecord.MyID}"/>
I created a demo to test your code, and it works properly. The full demo is here. I also added a function to verify the selected item.
If you want to get the SelectedItem object synchronously, the MyViewModel should implement INotifyPropertyChanged, and I created a selectedRecordfield for SelectedItem, so you can do like this:
public class MyViewModel : ViewModelBase
{
public MyViewModel()
{
// Sample to pre-load list of records from data server of KVP
//PickerChoices = GetDataFromServerForDemo("select * from LookupTable where Category = 'demo'");
PickerChoices = new ObservableCollection<TestModel>() {
new TestModel{MyID = 5, ShowValue="test1"}, new TestModel{MyID = 9, ShowValue="test2"},
new TestModel{MyID = 18, ShowValue="test18"}, new TestModel{MyID = 34, ShowValue="test4"}
};
// Set the default selected item
// foreach (TestModel model in PickerChoices) {
// if (model.MyID == 18) { // Default value
// SelectedRecord = model;
// break;
// }
// }
ShowThisRecord = new TestModel();
// For grins, I am setting the value that SHOULD be defaulted
// in picker. In this case, ID = 12 = "Some Item" from above
ShowThisRecord.MyID = 12;
}
// This is the record that has the "ID" column I am trying to bind to
public TestModel ShowThisRecord { get; set; }
//*****************************************
TestModel selectedRecord; // Selected item object
public TestModel SelectedRecord
{
get { return selectedRecord; }
set
{
if (selectedRecord != value)
{
selectedRecord = value;
OnPropertyChanged();
}
}
}
//*****************************************
// The picker is bound to this list of possible choices
public ObservableCollection<TestModel> PickerChoices { get; set; }
}
class ViewModelBase
public class ViewModelBase: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And the XAML content:
<Picker Title="Select a value" x:Name="mypicker"
ItemsSource="{Binding Path= PickerChoices}"
SelectedItem="{Binding SelectedRecord}"
ItemDisplayBinding="{Binding MyID}"/>
File xaml.cs:
public partial class MainPage : ContentPage
{
ObservableCollection<TestModel> items = new ObservableCollection<TestModel>();
MyViewModel testModel = null;
public MainPage()
{
InitializeComponent();
testModel = new MyViewModel();
BindingContext = testModel;
// This will also work
//if (testModel!=null && testModel.PickerChoices!=null) {
// for (int index=0; index< testModel.PickerChoices.Count; index++) {
// TestModel temp = testModel.PickerChoices[index];
// if (18 == temp.MyID) {
// mypicker.SelectedIndex = index;
// break;
// }
// }
//}
foreach (TestModel model in testModel.PickerChoices)
{
if (model.MyID == 18)
{ // Default value
testModel.SelectedRecord = model;
break;
}
}
}
// To show the selected item
private void Button_Clicked(object sender, EventArgs e)
{
if (testModel.SelectedRecord!=null) {
DisplayAlert("Alert", "selected Item MyID: " + testModel.SelectedRecord.MyID + "<--> ShowValue: " + testModel.SelectedRecord.ShowValue, "OK");
}
}
}
The result is:
You need to set the ItemsSource property to your list of CboIntValue items:
<Picker Title="Select a value"
ItemsSource="{Binding PickerChoices}"
ItemDisplayBinding="{Binding ShowValue}" />
After much work, I ended up writing my own separate class and template style for what I needed. Due to the length of it, and posting the source code for anyone to use, review, assess, whatever, I posted that out on The Code Project.
Again, the primary issue I had is if I have an integer key ID coming from a data source, the picker would not automatically refresh itself by just the given ID (int or string).
I'm stuck on following:
I have an application gui as follows:
I created a seperate class library for 'klant' which contains a string 'Naam' and an integer 'Consumpties'and linked this to my project.
When the user adds a name (Naam) and a number of consumptions (Consumpties), these values are addes to a listbox.
Here is my code so far:
private void btnKlantToevoegen_Click(object sender, RoutedEventArgs e)
{
List<Klant> klanten = new List<Klant>();
klanten.Add(new Klant
{
Naam = txtKlantNaam.Text,
Consumpties = int.Parse(txtKlantConsumpties.Text)
}
);
for (int i = 0; i < klanten.Count; i++)
{
lbOverzicht.Items.Add(klanten[i]);
}
I need to calculate the sum of the entered clients and consumptions, but i'm stuck on how to properly achieve this.
Also, there needs to be set a maximum number of consumptions per client and a maximum of total consumptions.
How to call this?
I tried creating a method to calculate the total of consumptions, but am not able to accomplish results. The code i have for this so far is as follows:
int BerekenTotaalConsumpties(int totaalconsumpties)
{
totaalconsumpties = 0;
int consumpties = int.Parse(txtKlantConsumpties.Text);
for (int i = 0; i <= consumpties; i++)
{
totaalconsumpties += Convert.ToInt32(lbOverzicht.Items[i].ToString());
}
return totaalconsumpties;
}
The result of this method then needs to be places in a label. I would do this as follows:
lblOverzicht.Content = BerekenTotaalConsumpties();
Klant class as follows:
public class Klant
{
public string Naam;
public int Consumpties;
public override string ToString()
{
return string.Format("{0} ({1})", this.Naam, this.Consumpties);
}
}
How can the names (Naam) be sorted alphabetically?
Untill now we have
lbOverzicht.Items.SortDescriptions.Add(new SortDescription("", ListSortDirection.Ascending));
But this doens't seem to work?
How can we verify that the users enters letters in field Naam and numbers in field Consumptions?
Use the power of MVVM and the CollectionChange event of an ObservableCollection to handle the summing of any and all changes to that collection.
In your view model (which adheres to INotifyPropertyChange) declare these properties
// Zehn Kleine Jagermeister!
public ObservableCollection<Klant> Overzicht
{
get { return _Overzicht; }
set { _Overzicht = value; OnPropertyChanged(); }
}
public int AantalKlanten { get { return Overzicht?.Count ?? 0; } }
public int AantalConsumpties { get; set; }
So we have the list of Klants and the totals. Then in the constructor of the viewmodel we allocate the observable collection and subscribe to the change of collection event:
Overzicht = new ObservableCollection<Klant>();
Overzicht.CollectionChanged += (o, args) =>
{
AantalConsumpties = Overzicht.Sum(klnt => klnt.Consumpties);
OnPropertyChanged("AantalConsumpties");
OnPropertyChanged("AantalKlanten");
};
So whenever we add or remove something from the collection, we recalculate the totals. By directly calling the OnPropertyChanged methods for the totals we announce to the XAML controls which have bound to those properties, that the values have changed and to update them for the user to see.
Here is the binding in XAML
<ListBox ItemsSource="{Binding Overzicht}"/>
<TextBlock Text="{Binding AantalKlanten }"/>
<TextBlock Text="{Binding AantalConsumpties}"/>
Then to limit things on add of the button click, simply check AantalKlanten and AantalCompties before allowing an insertion (Add) into the Overzicht collection.
The below shows the add in a button click with a max of 100. You can change it as needed.
if ((myViewModel.AantalConsumpties + newvalue) < 100)
myViewModel.Overzicht.Add(new Klant() { Naam = ..., Consumpties = newvalue});
This is the Klant
public class Klant
{
public string Naam { get; set; }
public int Consumpties { get; set; }
}
New to MVVM? Check out my quick example here Xaml: ViewModel Main Page Instantiation and Loading Strategy for Easier Binding for more information.
For the counting, try this:
int totalNaams = 0;
int totalConsumpties = 0;
foreach (Klant item in klanten)
{
totalConsumpties += item.Consumpties;
totalNaams += 0;
}
As for limiting the number of Consumpties per client, that needs to be done before you add it to the listbox. Just check to see if it's less than or equal to your max.
For limiting the total Comsumpties, You will need to calculate the new total after each new addition to the ListView. That would probably require a different approach to the counting so you're not executing the foreach more times that you need to.
New XAML
<ListView x:Name="lbOverzicht" HorizontalAlignment="Left" Height="200" Margin="435,372,0,0" VerticalAlignment="Top" Width="177">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Naam}" />
<GridViewColumn DisplayMemberBinding="{Binding Consumpties}" />
</GridView>
</ListView.View>
</ListView>
New Code-Behind
int totalNaams = 0;
int totalConsumpties = 0;
int consumptiesTotalMax = 0;
int consumptiesMax = 0;
List<Klant> klanten = new List<Klant>();
public class Klant
{
public string Naam { get; set; }
public int Consumpties { get; set; }
public override string ToString()
{
return string.Format("{0} ({1})", this.Naam, this.Consumpties);
}
}
public void btnKlantToevoegen_Click(object sender, RoutedEventArgs e)
{
int current = int.Parse(txtKlantConsumpties.Text);
consumptiesTotalMax = int.Parse(txtTotalMax.Text);
consumptiesMax = int.Parse(txtConsumpMax.Text);
if (!string.IsNullOrEmpty(txtKlantConsumpties.Text.Trim()) && !string.IsNullOrEmpty(txtKlantNaam.Text.Trim()))
{
if (((totalConsumpties + current) <= consumptiesTotalMax) && (current <= consumptiesMax))
{
klanten.Add(new Klant
{
Naam = txtKlantNaam.Text,
Consumpties = current
}
);
lbOverzicht.ItemsSource = klanten;
totalConsumpties += current;
totalNaams += 1;
CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(lbOverzicht.ItemsSource);
view.SortDescriptions.Add(new SortDescription("Naam", ListSortDirection.Ascending));
}
}
}
public int NumConsumptionsPerClient(string client)
{
var aux = listBox1.Items;
List<int> consumptions = new List<int>();
foreach (var i in aux)
{
if (i.ToString().Contains(client))
{
consumptions.Add(Convert.ToInt32(Regex.Match(i.ToString(), #"\d+").Value)); // only gets numbers
}
}
int sumconsumptions = 0;
foreach (int k in consumptions)
{
sumconsumptions = sumconsumptions + k;
}
return sumconsumptions;
}
public int NumConsumptions()
{
var aux = listBox1.Items;
List<int> consumptions = new List<int>();
foreach (var i in aux)
{
consumptions.Add(Convert.ToInt32(Regex.Match(i.ToString(), #"\d+").Value)); // only gets numbers
}
int sumconsumptions = 0;
foreach (int k in consumptions)
{
sumconsumptions = sumconsumptions + k;
}
return sumconsumptions;
}
public int NumClients()
{
return listBox1.Items.Count;
}
With first method you can control the max. consumptions per client. (you modify the method for your object instead of string)
Second and third methods are easy to understand.
I have a DataGrid in my View as shown below.,
My Question is how can I Append the values from the textboxes to the row datagrid
I have make sure that the Model has All the properties, When I click on the Add button it overwrites the dataGrid and shows only one latest record the and my ViewModel look like this:
class BatchItemsViewModel : ViewModelBase
{
public SearchItemsModel msearchItems { get; set; }
ObservableCollection<SearchItemsModel> _BatchItemsGrid;
public ObservableCollection<SearchItemsModel> BatchItemsGrid
{
get { return _BatchItemsGrid; }
set
{
_BatchItemsGrid = value;
OnPropertyChanged("BatchItemsGrid");
}
}
private ICommand _addDataToBatchGrid;
public ICommand addDataToBatchGrid
{
get
{
return _addDataToBatchGrid;
}
set
{
_addDataToBatchGrid = value;
}
}
public BatchItemsViewModel()
{
msearchItems = new SearchItemsModel();
addDataToBatchGrid = new RelayCommand(new Action<object>(AddDataInBatchGrid));
}
public void AddDataInBatchGrid(object obj)
{
ObservableCollection<SearchItemsModel> batchGridData = new ObservableCollection<SearchItemsModel>();
var data = new SearchItemsModel
{
BatchNumber = msearchItems.BatchNumber,
MFDDate = msearchItems.MFDDate,
ExpiryDate = msearchItems.ExpiryDate,
Quantity = msearchItems.Quantity,
};
batchGridData.Add(data);
BatchItemsGrid = batchGridData; // HERE I am overwriting the datagrid
//How can I Append the batchGridData to BatchItemsGrid (BatchItemsGrid.Append(batchGridData)???)
}
}
NOTE: I have gone through the other threads as well in the community for the similar posts but I couldn't find the appropriate and please correct me if I am going in wrong direction.
public void AddDataInBatchGrid(object obj)
{
var data = new SearchItemsModel
{
BatchNumber = msearchItems.BatchNumber,
MFDDate = msearchItems.MFDDate,
ExpiryDate = msearchItems.ExpiryDate,
Quantity = msearchItems.Quantity,
};
this.BatchItemsGrid.Add(data);
}
...Should do the trick. (don't replace the whole collection, just add items to it and let the notification events handle the UI updates)
I am using XamDataGrid and need each row to be hierarchical (with several child nodes that are also hierarchical). It must load its data on demand and have
dynamic child columns based on the results that are returned by a server I'm querying.
Fast forwarding a bit:
I have gone down the road of using ITypedList and ICustomPropertyDescriptor to dynamically add/remove properties so that the corresponding columns can be manipulated. To no avail.
I have tried dynamically changing FieldLayouts in the code behind, for each specific level of hierarchy, specified by ParentFieldLayoutKey. Yet, if I modify ANY of my field layouts, it applies the changes to all field layouts visually, but in the code behind, only the chosen field layout is actually modified.
Why can't I modify one FieldLayout without changing all of them?
I have also tried using a DataSet that contains tables with relationships to ESTABLISH the desired hierarchy,
but in doing so have thus far been unable to load data on demand. Unless there's some way to do that, that I haven't been able to find anywhere in the infragistics docs?
Here are my requirements:
Must be hierarchical
Must load data on demand (on expand)
Columns should not be known fully until runtime/on expand/when server is queried.
Questions:
Is it possible to achieve all of these things with XamDataGrid?
Edit: Yep. It's possible.
Here is my version. This basically does what you were looking for. In the child fieldlayout columns are dynamically added and data binding is performed accordingly
Models:
public class Country
{
private string _name;
public string Name
{
get { return this._name; }
set { this._name = value; }
}
public ObservableCollection<State> States { get; set; }
}
public class State
{
private readonly Dictionary<string, object> _info = new Dictionary<string, object>();
/// <summary>
/// Attributes of the state can be added dynamically (key will be the attribtue name etc...)
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public object this[string key]
{
get
{
return this._info[key];
}
set
{
this._info[key] = value;
}
}
public string StateName { get; set; }
}
Behaviour:
public class GridFieldLayoutBehaviour : Behavior<XamDataGrid>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.FieldLayoutInitialized += OnFieldLayoutInitialized;
this.AssociatedObject.RecordExpanded += OnRecordExpanded;
}
void OnRecordExpanded(object sender, Infragistics.Windows.DataPresenter.Events.RecordExpandedEventArgs e)
{
((ViewModel)this.AssociatedObject.DataContext).AddStateAttributes();
}
void OnFieldLayoutInitialized(object sender, Infragistics.Windows.DataPresenter.Events.FieldLayoutInitializedEventArgs e)
{
if( e.FieldLayout.ParentFieldName == "States")
{
((ViewModel)this.AssociatedObject.DataContext).GridFieldLayout = e.FieldLayout;
}
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.FieldLayoutInitialized -= OnFieldLayoutInitialized;
this.AssociatedObject.RecordExpanded -= OnRecordExpanded;
}
}
ViewModel:
public class ViewModel
{
#region Private Fields
private readonly ObservableCollection<Country> _countries = new ObservableCollection<Country>();
private readonly List<string> _stateTotalAttributes = new List<string>();
#endregion
public ViewModel()
{
FillData();
var stateAttributes = new string[] {"Population", "Unemployment Rate", "Capital", "Governor", "TimeZone", "Area"};
foreach (var stateAttribute in stateAttributes)
_stateTotalAttributes.Add(stateAttribute);
}
public ObservableCollection<Country> Countries
{
get { return this._countries; }
}
public FieldLayout GridFieldLayout { get; set; }
/// <summary>
/// Country and the states are populated
/// </summary>
private void FillData()
{
var country = new Country();
country.States = new ObservableCollection<State>();
country.Name = "USA";
var xdoc = XDocument.Load("states_data.xml");
foreach (var state in xdoc.Descendants("states").Descendants())
{
var newState = new State();
newState.StateName = state.Attributes("name").FirstOrDefault().Value;
newState["Unemployment Rate"] = state.Attributes("unemployment-rate").FirstOrDefault().Value;
newState["Capital"] = state.Attributes("capital").FirstOrDefault().Value;
newState["Governor"] = state.Attributes("governor").FirstOrDefault().Value;
newState["Area"] = state.Attributes("area").FirstOrDefault().Value;
newState["TimeZone"] = state.Attributes("timezone").FirstOrDefault().Value;
newState["Population"] = state.Attributes("population").FirstOrDefault().Value;
country.States.Add(newState);
}
_countries.Add(country);
}
public void AddStateAttributes()
{
GridFieldLayout.Fields.BeginUpdate();
// Remove the current columns (except for StateName)
var removableFields = GridFieldLayout.Fields.Where(f => f.Name != "StateName");
removableFields.ToList().ForEach(field => GridFieldLayout.Fields.Remove(field));
// Figure out what state attributes to add
var random = new Random(DateTime.Now.Millisecond);
var numCols = random.Next(1, 6);
var colsToAdd = GetStateAttributes(numCols, random);
// Finally add the new ones'
foreach (var col in colsToAdd)
{
var field = new UnboundField();
field.Name = col;
field.Binding = new Binding()
{
Mode = BindingMode.TwoWay,
Path = new PropertyPath(string.Format("[{0}]", col)),
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};
GridFieldLayout.Fields.Add(field);
}
GridFieldLayout.Fields.EndUpdate();
}
private List<string> GetStateAttributes(int numCols, Random random)
{
List<string> colsToAdd = new List<string>();
for( int i = 0; i < numCols; i++)
{
int idx = random.Next(1, 6) - 1;
if(colsToAdd.Contains(_stateTotalAttributes[idx]) == false)
{
colsToAdd.Add(_stateTotalAttributes[idx]);
}
}
return colsToAdd;
}
}
XAML:
<igDP:XamDataGrid DataSource="{Binding Countries}" >
<i:Interaction.Behaviors>
<local:GridFieldLayoutBehaviour/>
</i:Interaction.Behaviors>
</igDP:XamDataGrid>
States_Data.xml:
<states>
<state name="New York" population="19,651,127" unemployment-rate="" capital="Albany" governor="Andrew Cuomo" timezone="EST" area="54,556 sq mi"></state>
<state name="New Hampshire" population="1,323,459 " unemployment-rate="" capital="Concord" governor="Maggie Hassan" timezone="EST" area="9,304 sq mi"></state>
</states>
Regards,
Vishwa
Figured it out.
Since I can't paste the code (technically, it belongs to the company I work for), I guess I'll just have to settle with explaining BASICALLY what I did.
I made two wrappers, one to wrap my object to expose/create hierarchy (AutoGeneratedColumns="True", by default, assumes that a collection of objects inside of your object is your object's CHILDREN), and an ITypedList to wrap the new wrapper, so you can dynamically add properties to it.
I hope this post was at least somewhat informative.