How to change datacontext at runtime with Mvvm - wpf

I have a graph that I want to change some ViewModel property so the whole graph would change accordingly.
The only property that I want to change here is "year", I tried to implement the INotifyPropertyChanged so the binding will cause the graph to change automatically, but it didn't work.
This is the model:
public class Model
{
public double rate { get; set; }
public string date { get; set; }
}
This is the ViewModel:
public class ViewModel :INotifyPropertyChanged
{
private string _year;
public string Year { get { return _year; } set { _year = value;UpdateData(); OnPropertyChanged("Year"); } }
public ViewModel()
{
_year = "2017";
UpdateData();
}
public void UpdateData()
{
int i,j;//Indexs that holds actuall api retrived values
string cR, urlContents;// cR- current rate in string format, urlContents - the whole Api retrived data
string c;//For api syntx, add 0 or not, depends on the current date syntax
this.CurrenciesHis = new ObservableCollection<Model>();//Model objects collection
HttpClient client = new HttpClient();
for (int l = 1; l < 13; l++)
{
if (l < 10)
c = "0";
else
c = "";
urlContents = client.GetStringAsync("http://data.fixer.io/api/"+(_year)+"-"+ c + l + "-01?access_key=&base=USD&symbols=EUR&format=1").Result;
i = urlContents.IndexOf("EUR");//Finds the desired value from api recived data
j = urlContents.IndexOf("}");
cR = urlContents.Substring(i + 5, (j - 2) - (i + 5));
CurrenciesHis.Add(new Model() { rate = Convert.ToDouble(cR), date = "01/" + l.ToString() });
}
}
public ObservableCollection<Model> CurrenciesHis { get; set; }
#region "INotifyPropertyChanged members"
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
#endregion
This is the View that based on third party control (I deleted alot of XAML and used bold letters to mark where is the actuall binding located):
<layout:SampleLayoutWindow x:Class="AreaChart.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
ResizeMode="CanResizeWithGrip"
xmlns:chart="clr-namespace:Syncfusion.UI.Xaml.Charts;assembly=Syncfusion.SfChart.WPF"
xmlns:local="clr-namespace:PL"
xmlns:layout="clr-namespace:Syncfusion.Windows.SampleLayout;assembly=Syncfusion.Chart.Wpf.SampleLayout"
UserOptionsVisibility="Collapsed"
WindowStartupLocation="CenterScreen" Height="643.287" Width="1250.5"
Title="2017">
<Grid>
<Grid.Resources>
...........................................
<chart:AreaSeries x:Name="AreaSeries" EnableAnimation="True"
**XBindingPath="date"
Label="Favourite"
YBindingPath="rate"
ItemsSource="{Binding CurrenciesHis}"**
ShowTooltip="True" >
<chart:AreaSeries.AdornmentsInfo>
<chart:ChartAdornmentInfo AdornmentsPosition="Bottom"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ShowLabel="True">
<chart:ChartAdornmentInfo.LabelTemplate>
<DataTemplate>
....................................
<TextBox HorizontalAlignment="Left" Height="30" Margin="28,231,0,0" TextWrapping="Wrap" Name="Text1" VerticalAlignment="Top" Width="76" Text="{Binding Year, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
This is the code behaind and the event of the Textbox that I want to change with it's help that year property of the viewmodel:
public partial class MainWindow : SampleLayoutWindow
{
PL.ViewModel newInstance;
public MainWindow()
{
InitializeComponent();
newInstance = new PL.ViewModel();
this.DataContext = newInstance;
}
}
What I understand is that from this point the mechanism of WPFwill change the values on the chart using the binding and the "notification" of INotifyPropertyChanged but it doesn't work for me..

year should be a private field, but it is public. You're setting the value of the field, which naturally does not execute any of the code in the setter for the property.
Make year and all of your backing fields private, and rename all of your private fields with a leading underscore (for example, year should be renamed to _year) to prevent accidents like this.
And make it a policy in your viewmodel code always to set the property, never the field, except of course inside the actual property setter for that field.
Also, use bindings to set viewmodel properties from UI. Don't do it in codebehind. Get rid of that textchanged handler.
<TextBox
HorizontalAlignment="Left"
Height="30"
Margin="28,231,0,0"
TextWrapping="Wrap"
VerticalAlignment="Top"
Width="76"
Text="{Binding Year, UpdateSourceTrigger=PropertyChanged}"
/>
Finally, it seems that you intended for changes to Year to have some effect on the contents of CurrenciesHis, but there's no mechanism for that in your code, and no explanation of what you want to have happen or how you expect it to happen.
And here's an updated version of your viewmodel.
public class ViewModel
{
public ViewModel()
{
// DO NOT, DO NOT EVER, DO NOT, SERIOUSLY, EVER, EVER, EVER UPDATE A
// PROPERTY'S BACKING FIELD OUTSIDE THE PROPERTY'S SETTER.
Year = DateTime.Now.Year - 1;
UpdateCurrencies();
}
protected void UpdateCurrencies()
{
// Indexs that holds actuall api retrived values
int i, j;
// cR- current rate in string format, urlContents - the whole Api retrived data
string cR, urlContents;
// For api syntx, add 0 or not, depends on the current date syntax
string c;
CurrenciesHis = new ObservableCollection<Model>();//Model objects collection
HttpClient client = new HttpClient();
for (int l = 1; l < 13; l++)
{
if (l < 10)
c = "0";
else
c = "";
// Use the public property Year, not the field _year
var url = "http://data.fixer.io/api/" + Year + "-" + c + l + "-01?access_key=&base=USD&symbols=EUR&format=1";
urlContents = client.GetStringAsync(url).Result;
i = urlContents.IndexOf("EUR");//Finds the desired value from api recived data
j = urlContents.IndexOf("}");
cR = urlContents.Substring(i + 5, (j - 2) - (i + 5));
CurrenciesHis.Add(new Model() { rate = Convert.ToDouble(cR), date = "01/" + l.ToString() });
}
OnPropertyChanged(nameof(CurrenciesHis));
}
// Year is an integer, so make it an integer. The binding will work fine,
// and it'll prevent the user from typing "lol".
private int _year;
public int Year
{
get { return _year; }
set
{
if (_year != value)
{
_year = value;
OnPropertyChanged(nameof(Year));
UpdateCurrencies();
}
}
}
public ObservableCollection<Model> CurrenciesHis { get; private set; }
// -----------------------------------------------------
// YearsList property for ComboBox
// 30 years, starting 30 years ago.
// You could make this IEnumerable<int> or ReadOnlyCollection<int> if you
// want something other than the ComboBox to use it. The ComboBox doesn't care.
// Year MUST be an int for the binding to SelectedItem (see below) to work,
// not a string.
public System.Collections.IEnumerable YearsList
=> Enumerable.Range(DateTime.Now.Year - 30, 30).ToList().AsReadOnly();
}
XAML for YearsList combobox (which I prefer to the text box btw):
<ComboBox
ItemsSource="{Binding YearsList}"
SelectedItem="{Binding Year}"
/>

Your CurrenciesHis property doesn't implement INPC so WPF doesn't realize that you changed it (UpdateData() has "this.CurrenciesHis = new ObservableCollection();")
Your current property is:
public ObservableCollection<Model> CurrenciesHis { get; set; }
Should be something like this:
private ObservableCollection<Model> _CurrenciesHis;
public ObservableCollection<Model> CurrenciesHis { get { return _CurrenciesHis; } set { if (_CurrenciesHis != value) { _CurrenciesHis = value; OnPropertyChanged("CurrenciesHis"); } } }

Related

How i can filter dataGrid on the basis of Column header in wpf? [duplicate]

I search an example or sample to filter the WPF DataGrid column elements by a textbox.
Something similar to this (the given example uses a WPFToolkit... apparently abandoned by Microsoft...)
XAML
<Canvas>
<DataGrid Height="200" Name="dataGrid1" Width="200" Canvas.Top="23" />
<TextBox Name="textBox1" Width="120" />
</Canvas>
cs:
public partial class MainWindow : Window
{
private List<Personne> persons;
ICollectionView cvPersonnes;
public MainWindow()
{
InitializeComponent();
persons = new List<Personne>();
persons.Add(new Personne() { Id = 1, Nom = "Jean-Michel", Prenom = "BADANHAR" });
persons.Add(new Personne() { Id = 1, Nom = "Gerard", Prenom = "DEPARDIEU" });
persons.Add(new Personne() { Id = 1, Nom = "Garfild", Prenom = "THECAT" });
persons.Add(new Personne() { Id = 1, Nom = "Jean-Paul", Prenom = "BELMONDO" });
cvPersonnes = CollectionViewSource.GetDefaultView(persons);
if (cvPersonnes != null)
{
dataGrid1.AutoGenerateColumns = true;
dataGrid1.ItemsSource = cvPersonnes;
cvPersonnes.Filter = TextFilter;
}
}
public bool TextFilter(object o)
{
Personne p = (o as Personne);
if (p == null)
return false;
if (p.Nom.Contains(textBox1.Text))
return true;
else
return false;
}
}
public class Personne
{
public int Id { get; set; }
public string Nom { get; set; }
public string Prenom { get; set; }
}
You can filter the Items in the DataGrid by binding it to an ICollectionView that supports filtering.
Details here for .NET 4. The process is the same for .NET 4.5, but it seems the documentation has been lost. There's a small mention to it here under the "Grouping, Sorting, and Filtering" heading.
edit: at the time this was originally written, the WPF toolkit had not been abandoned by Microsoft. The controls that used to be part of it are now in the framework, and the toolkit was alive and doing well here
I have seen at various sites much ado about this matter...
To filter the latter being a datagrid using a datatable as the source, which is quite common to make the code below:
DataTable dt = new DataTable("Table1");
//fill your datatable...
//after fill...
dataGrid1.DataContext = dt;
IBindingListView blv = dt.DefaultView;
blv.Filter = "NAME = 'MOISES'";
There are several solutions, but in my opinion, the best solutions are the ones which uses only DataGrid styles without inventing a new inherited DataGird type. The followings are the best I found:
Option 1: which I personally use: Automatic WPF Toolkit DataGrid Filtering
Option 2: Auto-filter for Microsoft WPF DataGrid
I have written my own FilterDataGrid Control, it's much more flexible than the ones provided on CodeProject or elsewhere. I can neither post the full code here, nor can I publish it.
But: Since your datasource is most likely wrapped into a ICollectionView, you can do something like this:
public void ApplyFilters()
{
ICollectionView view = CollectionViewSource.GetDefaultView(ItemsSource);
if (view != null)
{
view.Filter = FilterPredicate;
}
}
private bool FilterPredicate(object item)
{
var yourBoundItemOrRow = item as BoundItemType;
return aFilterObject.Matches(yourBoundItemOrRow);
}
You can implement any filter logic easily based on this concept. Even very, very powerful filters. Note: I have those methods in my own class derived from datagrid. They can be adapted to work outside of the grid, too, for example in a UserControl

WPF/MVVM: Property Declaration Requiring Supporting Method/Function Logic

I have two properties I'm setting for two WPF combo boxes: one for Month and one for Day. The MonthIndex property looks like this:
private int monthIndex = DateTime.Today.Month - 1;
public int MonthIndex
{
get { return monthIndex; }
set
{
if (monthIndex != value)
{
monthIndex = value;
RaisePropertyChanged("MonthIndex");
}
}
}
I need to set the DayIndex property but unlike the Month property, it requires calculation - can't use simple declaration like
private int _dayIndex = DateTime.Today.Day - 1;
Each calendar day can have 0 or more events. e.g., if 6/30 had three events, such events would be stored as 30, 30.1, and 30.2 (in ObservableCollection DaysList and corresponding index for each event).
Below is the XAML, declaration, and method for DayIndex:
View:
<ComboBox Name="cboDay"
ItemsSource="{Binding DaysList, Mode=OneTime}"
DisplayMemberPath="fltDay"
SelectedIndex="{Binding DayIndex, Mode=TwoWay}"
IsEditable="True" />
ViewModel:
public ObservableCollection<Day> DaysList { get; set; }
private int _dayIndex;
public int DayIndex
{
get
{
// perform some calculation logic;
return _dayIndex;
}
set
{
if (_dayIndex != value)
{
_dayIndex = value;
RaisePropertyChanged("DayIndex");
}
}
}
How do I handle the declaration for dayIndex so it remains updated as the monthIndex does (so I can use its value with other code)?

Databinding a WinForms ComboBox requires user-interaction twice

I have a ComboBox (DropDownList style) in a Windows Form which has a data-source set to a BindingList, and has the SelectedValue property bound to a viewmodel's property.
Note that the binding is set to OnPropertyChanged rather than OnValidate, this is because when using OnValidate the control will not necessarily update the ViewModel if the form is closed or loses focus (but the control still thinks it has focus. On the Compact Framework there is no way to 'force validation' so I have to use OnPropertyChanged.
There's a problem which is reproducible on both desktop Windows Forms and Smart Device Windows Forms: when attempting to select or set the current item in the combobox (using the mouse or keyboard) the value will not "stick" until it is set twice - that is, you need to select the same item twice before the combobox's value will change.
There are no exceptions thrown (even caught exceptions) and no diagnostics reports to speak of.
I don't think this is a bug in the framework, and it's interesting how it happens on both Desktop and Compact Framework.
Here's my code:
Form1.cs
public partial class Form1 : Form {
private ViewModel _vm;
public Form1() {
InitializeComponent();
this.bindingSource1.Add( _vm = new ViewModel() );
}
}
Form1.Designer.cs (relevant lines)
//
// bindingSource1
//
this.bindingSource1.DataSource = typeof( WinForms.Shared.ViewModel );
//
// comboBox1
//
this.comboBox1.DataBindings.Add( new System.Windows.Forms.Binding( "SelectedValue", this.bindingSource1, "SelectedSomeTypeId", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged ) );
this.comboBox1.DataSource = this.someTypeListBindingSource;
this.comboBox1.DisplayMember = "DisplayText";
this.comboBox1.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.comboBox1.FormattingEnabled = true;
this.comboBox1.Location = new System.Drawing.Point( 12, 27 );
this.comboBox1.Name = "comboBox1";
this.comboBox1.Size = new System.Drawing.Size( 182, 21 );
this.comboBox1.TabIndex = 0;
this.comboBox1.ValueMember = "Id";
//
// someTypeListBindingSource
//
this.someTypeListBindingSource.DataMember = "SomeTypeList";
this.someTypeListBindingSource.DataSource = this.bindingSource1;
ViewModel.cs
public class ViewModel : INotifyPropertyChanged {
public ViewModel() {
this.SomeTypeList = new BindingList<SomeType>();
for(int i=0;i<5;i++) {
this.SomeTypeList.Add( new SomeType() {
Id = i + 1,
Name = "Foo" + ((Char)( 'a' + i )).ToString()
} );
}
this.SelectedSomeTypeId = 2;
}
public BindingList<SomeType> SomeTypeList { get; private set; }
private Int64 _selectedSomeTypeId;
public Int64 SelectedSomeTypeId {
get { return _selectedSomeTypeId; }
set {
if( _selectedSomeTypeId != value ) {
_selectedSomeTypeId = value;
OnPropertyChanged("SelectedSomeTypeId");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(String propertyName) {
PropertyChangedEventHandler handler = this.PropertyChanged;
if( handler != null ) handler( this, new PropertyChangedEventArgs(propertyName) );
}
}
public class SomeType {
public String Name { get; set; }
public Int64 Id { get; set; }
public String DisplayText {
get { return String.Format("{0} - {1}", this.Id, this.Name ); }
}
}
I have never found the 'right' way around this issue and generally use one of two ways to make things work:
Direct: Just bypass the binding mechanism for this one entry
combo1.SelectedIndexChanged += (s,e) _viewModel.Item = combo1.SelectedItem;
Generic Binding: Make a custom ComboBox and override the OnSelectedIndexChanged event to force the binding update.
public class BoundComboBox : ComboBox
{
protected override void OnSelectedIndexChanged(EventArgs e)
{
var binding = this.DataBindings["SelectedItem"];
if( binding != null )
binding.WriteValue();
base.OnSelectedIndexChanged(e);
}
}

Advice on viewmodel and inter-connected combo boxes

I am having a few struggles on how to wire up my interconnected comboboxes when using MVVM. I have a DTO that represents an Order which contains a CustomerId and an OrderTypeId. Thesere are then wrapped inside an OrderViewModel. I also have an EditOrderViewModel which loads from a db a list of Customers.
What I would like to do is Load an Order from the DB (similar to the Load function) choose the right item in the ComboBox (the items source of which is a List, display the name of the selected customer in a text block to the right of the combobox and finally load the list of OrderTypes that belong to that Customer in the next combobox and again select the correct OrderType and display the OrderTypeName in a TextBlock to the right.
I have managed to get some of the behaviour to work when I use SelectedItem from the combobox but this is only when I select the item manually as I am not sure how in my viewmodel I can convert Order.CustomerId (type int) into the correct SelectedItem (type CustomerDTO). Below is some code which shows generally what I am trying to achieve whilst using in-memory datasources. Thanks Alex
<ComboBox Height="25" Width="150" ItemsSource="{Binding Path=Customers}" SelectedValue="{Binding Path=Order.CustomerId}" SelectedValuePath="Id">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Code}"></TextBlock>
<TextBlock Text="{Binding Path=Name}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="{Binding Path=Name,Mode=OneWay,NotifyOnSourceUpdated=True}"></TextBlock>
public class EditOrderViewModel : VMBase
{
public OrderViewModel Order{get;set;}
public void Load()
{
Order = new OrderViewModel(new OrderDto{CustomerId=1,OrderTypeId=2});
Order.PropertyChanged += MainWindowViewModel_PropertyChanged;
}
public EditOrderViewModel()
{
Order = new OrderViewModel(new OrderDto());
Order.PropertyChanged += OrderViewModel_PropertyChanged;
Customers = new List<CustomerDto> {
new CustomerDto{ Id = 1, Code = "ACME", Name = "ACME CORP" },
new CustomerDto{ Id = 2, Code = "MSFT", Name="MICROSOFT CORP" },
new CustomerDto{ Id = 3, Code = "APP", Name = "APPLE" }};
OrderTypes = new List<OrderTypeDto>{
new OrderTypeDto{OrderTypeId=1, CustomerId =1, Name = "Cake Order"},
new OrderTypeDto{OrderTypeId=2, CustomerId =1, Name = "Sandwich Order"},
new OrderTypeDto{OrderTypeId=3, CustomerId =2, Name = "Chocolate Order"},
new OrderTypeDto{OrderTypeId=4, CustomerId =2, Name = "Bread Order"},
new OrderTypeDto{OrderTypeId=5, CustomerId =3, Name = "Drinks Order"}};
}
void OrderViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "OrderTypeId":
break;
case "SelectedCustomer":
break;
default:
break;
}
}
public List<OrderTypeDto> OrderTypes { get; set; }
public List<CustomerDto> Customers { get; set; }
}
public class OrderDto
{
public int CustomerId { get; set; }
public int OrderTypeId { get; set; }
}
public class CustomerDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
}
public class OrderViewModel : VMBase
{
private OrderDto _orderDto;
private string _customerName;
public OrderViewModel(OrderDto orderDto)
{
_orderDto = orderDto;
}
public int CustomerId {
get { return _orderDto.CustomerId; }
set
{
_orderDto.CustomerId = value;
RaisePropertyChanged("CustomerId");
}
}
public string CustomerName {
get { return _customerName; }
set {_customerName = value;
RaisePropertyChanged("CustomerName");
}
}
public int OrderTypeId
{
get { return _orderDto.OrderTypeId; }
set
{
_orderDto.OrderTypeId = value;
RaisePropertyChanged("OrderTypeId");
}
}
}
Don't set ComboBox.SelectedValue in your XAML binding. You should be binding ComboBox.SelectedItem to your model so you can have the CustomDTO easily available. You should add a property to your OrderViewModel called Customer (of type CustomerDTO) instead of trying to recreate the CustomerDTO using several properties (CustomerID, CustomerName, etc.).

Filter a DataGrid on a Text box

I search an example or sample to filter the WPF DataGrid column elements by a textbox.
Something similar to this (the given example uses a WPFToolkit... apparently abandoned by Microsoft...)
XAML
<Canvas>
<DataGrid Height="200" Name="dataGrid1" Width="200" Canvas.Top="23" />
<TextBox Name="textBox1" Width="120" />
</Canvas>
cs:
public partial class MainWindow : Window
{
private List<Personne> persons;
ICollectionView cvPersonnes;
public MainWindow()
{
InitializeComponent();
persons = new List<Personne>();
persons.Add(new Personne() { Id = 1, Nom = "Jean-Michel", Prenom = "BADANHAR" });
persons.Add(new Personne() { Id = 1, Nom = "Gerard", Prenom = "DEPARDIEU" });
persons.Add(new Personne() { Id = 1, Nom = "Garfild", Prenom = "THECAT" });
persons.Add(new Personne() { Id = 1, Nom = "Jean-Paul", Prenom = "BELMONDO" });
cvPersonnes = CollectionViewSource.GetDefaultView(persons);
if (cvPersonnes != null)
{
dataGrid1.AutoGenerateColumns = true;
dataGrid1.ItemsSource = cvPersonnes;
cvPersonnes.Filter = TextFilter;
}
}
public bool TextFilter(object o)
{
Personne p = (o as Personne);
if (p == null)
return false;
if (p.Nom.Contains(textBox1.Text))
return true;
else
return false;
}
}
public class Personne
{
public int Id { get; set; }
public string Nom { get; set; }
public string Prenom { get; set; }
}
You can filter the Items in the DataGrid by binding it to an ICollectionView that supports filtering.
Details here for .NET 4. The process is the same for .NET 4.5, but it seems the documentation has been lost. There's a small mention to it here under the "Grouping, Sorting, and Filtering" heading.
edit: at the time this was originally written, the WPF toolkit had not been abandoned by Microsoft. The controls that used to be part of it are now in the framework, and the toolkit was alive and doing well here
I have seen at various sites much ado about this matter...
To filter the latter being a datagrid using a datatable as the source, which is quite common to make the code below:
DataTable dt = new DataTable("Table1");
//fill your datatable...
//after fill...
dataGrid1.DataContext = dt;
IBindingListView blv = dt.DefaultView;
blv.Filter = "NAME = 'MOISES'";
There are several solutions, but in my opinion, the best solutions are the ones which uses only DataGrid styles without inventing a new inherited DataGird type. The followings are the best I found:
Option 1: which I personally use: Automatic WPF Toolkit DataGrid Filtering
Option 2: Auto-filter for Microsoft WPF DataGrid
I have written my own FilterDataGrid Control, it's much more flexible than the ones provided on CodeProject or elsewhere. I can neither post the full code here, nor can I publish it.
But: Since your datasource is most likely wrapped into a ICollectionView, you can do something like this:
public void ApplyFilters()
{
ICollectionView view = CollectionViewSource.GetDefaultView(ItemsSource);
if (view != null)
{
view.Filter = FilterPredicate;
}
}
private bool FilterPredicate(object item)
{
var yourBoundItemOrRow = item as BoundItemType;
return aFilterObject.Matches(yourBoundItemOrRow);
}
You can implement any filter logic easily based on this concept. Even very, very powerful filters. Note: I have those methods in my own class derived from datagrid. They can be adapted to work outside of the grid, too, for example in a UserControl

Resources