John Merc
John Merc

Reputation: 53

How to change datacontext at runtime with Mvvm

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..

Upvotes: 0

Views: 4779

Answers (2)

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}"
    />

Upvotes: 1

J.H.
J.H.

Reputation: 4322

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"); } } }

Upvotes: 0

Related Questions