Search code examples
c#wpfxamldata-bindingswitch-statement

Selected value from ComboBox not being passed to method in WPF MVVM application


I’m building a compound interest calculator using WPF and the MVVM pattern. I have a ComboBox in my view that allows the user to select the compound interval (e.g. “Daily”, “Weekly”, “Monthly”, etc.). The ItemsSource for this ComboBox is bound to an ObservableCollection property in my view model, and the SelectedItem is bound to a SelectedCompoundInterval property.

When the user makes a selection from the ComboBox, I expect the value of the SelectedCompoundInterval property to be updated accordingly. However, when I call a Calculate method in my view model that uses this property as an input to a calculation, the value of the SelectedCompoundInterval property is always empty.

MyViewModel Class

namespace CompoundInterestCalculator
{
    internal class MyViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private Calculations _calculations = new Calculations();

            private bool _showResults;
            private decimal _initialAmount;
            public decimal InitialAmount
            {
                get { return _initialAmount; }
                set
                {
                    _initialAmount = value;
                Debug.WriteLine(_initialAmount.ToString());
                    OnPropertyChanged(nameof(InitialAmount));
                }
            }

            private decimal _interestRate;
            public decimal InterestRate
            {
                get { return _interestRate; }
                set
                {
                    _interestRate = value;
                    OnPropertyChanged(nameof(InterestRate));
                }
            }

            private int _years;
            public int Years
            {
                get { return _years; }
                set
                {
                    _years = value;
                    OnPropertyChanged(nameof(Years));
                }
            }

            private int _months;
            public int Months
            {
                get { return _months; }
                set
                {
                    _months = value;
                    OnPropertyChanged(nameof(Months));
                }
            }

            public ObservableCollection<string> CompoundIntervals { get; } = new ObservableCollection<string>
    {
        "Daily","Weekly", "Monthly", "Quarterly", "Yearly"

    };

            private string _selectedCompoundInterval;
            public string SelectedCompoundInterval
        {
            get { return _selectedCompoundInterval; }
            set
            {
                if (_selectedCompoundInterval != value)
                {
                    _selectedCompoundInterval = value;
                    OnPropertyChanged(nameof(SelectedCompoundInterval));

                    Debug.WriteLine("SelectedCompoundInterval changed: " + value);
                }

            }
        }

        public ObservableCollection<string> InterestRateTypes { get; } = new ObservableCollection<string>
{
    "Percent", "Decimal"
};

        private string _selectedInterestRateType;
            public string SelectedInterestRateType
            {
                get { return _selectedInterestRateType; }
                set
                {
                    _selectedInterestRateType = value;
                    OnPropertyChanged(nameof(SelectedInterestRateType));
                }
            }


            public DataTemplate CurrentTemplate
            {
                get
                {
                    if (_showResults)
                        return (DataTemplate)Application.Current.FindResource("ResultsTemplate");
                    else
                        return (DataTemplate)Application.Current.FindResource("InputTemplate");
                }
            }

            public void Calculate()
            {
            Debug.WriteLine("CompoundIntervals: " + string.Join(", ", CompoundIntervals));
            Debug.WriteLine("SelectedCompoundInterval: " + SelectedCompoundInterval);
            // Perform calculations here
            decimal initialAmount = InitialAmount;
            decimal interestRate = InterestRate;
            int years = Years;
            int months = Months;
            string compoundInterval = SelectedCompoundInterval;
            Debug.WriteLine("compoundInterval: " + compoundInterval);
            string interestRateType = SelectedInterestRateType;

            decimal totalAmount = _calculations.CalculationsAmounts(initialAmount, interestRate, years, months, compoundInterval, interestRateType);


            
                // Show results
                _showResults = true;
                OnPropertyChanged(nameof(CurrentTemplate));
            }


            
        }

    }

Calculations class

namespace CompoundInterestCalculator
{
    
    internal class Calculations
    {

       
        public decimal CalculationsAmounts(decimal initialAmount, decimal interestRate, int years, int months, string compoundInterval, string interestRateType)
        {

            Debug.WriteLine("compoundInterval: " + compoundInterval);

            int compoundFrequency;
            switch (compoundInterval)
            {
                case "Daily":
                    compoundFrequency = 365;
                    break;
                case "Weekly":
                    compoundFrequency = 52;
                    break;
                case "Monthly":
                    compoundFrequency = 12;
                    break;
                case "Quarterly":
                    compoundFrequency = 4;
                    break;
                case "Yearly":
                    compoundFrequency = 1;
                    break;
                default:
                    throw new ArgumentException("Invalid compound interval");

            }

            if (interestRateType == "Percent")
            {
                interestRate /= 100;
            }

            decimal totalAmount = initialAmount * (decimal)Math.Pow((double)(1 + interestRate / compoundFrequency), years * compoundFrequency + months * (compoundFrequency / 12));

            return totalAmount;
        }
    }
}

MainWindow.xaml.cs code

namespace CompoundInterestCalculator
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private MyViewModel _myViewModel;
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MyViewModel();
            _myViewModel = new MyViewModel();
            
        }

        private void CalculateButton_Click(object sender, RoutedEventArgs e)
        {
            MyContentControl.ContentTemplate = (DataTemplate)FindResource("ResultsTemplate");
            _myViewModel.Calculate();
            
        }
        private void BackButton_Click(object sender, RoutedEventArgs e)
        {
            MyContentControl.ContentTemplate = (DataTemplate)FindResource("InputTemplate");
        }

        private void NumericOnly(object sender, TextCompositionEventArgs e)
        {
            e.Handled = IsTextNumeric(e.Text);
        }

        private static bool IsTextNumeric(string str)
        {
            System.Text.RegularExpressions.Regex reg = new System.Text.RegularExpressions.Regex("[^0-9]");
            return reg.IsMatch(str);
        }
        private void NumericDecimalOnly(object sender, TextCompositionEventArgs e)
        {
            TextBox textBox = sender as TextBox;
            if (textBox != null)
            {
                string newText = textBox.Text.Insert(textBox.CaretIndex, e.Text);
                decimal value;
                if (!decimal.TryParse(newText, out value) || value < 0.01m || value > 99.9999999m)
                {
                    e.Handled = true;
                }
            }
        }
        private void NumericMonthlyOnly(object sender, TextCompositionEventArgs e)
        {
            TextBox textBox = sender as TextBox;
            if (textBox != null)
            {
                string newText = textBox.Text.Insert(textBox.CaretIndex, e.Text);
                int value;
                if (!int.TryParse(newText, out value) || value < 1 || value > 12)
                {
                    e.Handled = true;
                }
            }
        }


    }
}

MainWindow.xaml ComboBox code

 <ComboBox Grid.Column="1" ItemsSource="{Binding CompoundIntervals}" SelectedItem="{Binding SelectedCompoundInterval, Mode=TwoWay}" Margin="10,5,0,0" Width="100" Height="20">
                    </ComboBox>

I tried debugging the program in multiple places, I noticed that the ObservableCollection populates the combobox correctly and also when debugging the SelectedCompoundInterval property it does notify each time I change it inside the program signaling to me that the compoundInterval string used in my calculations should have something to store.

I assume this means the problem is somewhere between that point and when I calculate because debugging the compoundInterval string in my calculations code always shows a Null value and then the switch statement throws an argumentexception because it doesn't fit into any of the cases being a Null value.


Solution

  • Error in initializing ViewModel instance:

        public partial class MainWindow : Window
        {
            private readonly MyViewModel _myViewModel;
            public MainWindow()
            {
                InitializeComponent();
    
                // The field and the DataContext must have the same instance.
                _myViewModel = new MyViewModel();
                DataContext = _myViewModel;           
            }
    

    But I would recommend initializing the instance in XAML. This will make it easier to create layouts in the Visual Studio Designer.
    One of the possible variant:

        <Window.DataContext>
            <local:MyViewModel/>
        </Window.DataContext>
    
        public partial class MainWindow : Window
        {
            private readonly MyViewModel _myViewModel;
            public MainWindow()
            {
                InitializeComponent();
    
                _myViewModel = (MyViewModel) DataContext;           
            }