Search code examples
wpfmvvmdata-bindingtreeview

WPF - I am trying to bind a progressbar value to a Method that calculates a percentage based on 2 textboxes - all inside a treeviewitem


My specific problem at the moment is I am trying to update a progress bar by binding it to a method which returns an int.

I'm trying to make a basic Microsoft Project style task manager in WPF as a learning exercise. I'm very new to C#, WPF and the MVVM pattern. I'm adding 'Tasks' to a tree view with an 'Add' button that creates children for that 'Task'. I then want to be able to put in numbers for 'Total Hours' and 'Hours Remaining' inside Text Boxes and then see the percentage of the task done in a progress bar. At the moment when I put in numbers nothing is happening and I really don't know what I'm doing.

enter image description here

MainWindow.xaml

<Window x:Class="Project_Management_App___Test_02.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Project_Management_App___Test_02"
        xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
        DataContext="{Binding RelativeSource={RelativeSource self}}"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <StackPanel>
        <DockPanel>
            <Menu DockPanel.Dock="Top">
                <MenuItem Header="_File">
                    <MenuItem Header="New" Click="MenuItem_Click" />
                    <MenuItem Header="Open" />
                    <MenuItem Header="Save" />
                    <Separator />
                    <MenuItem Header="Exit" />
                </MenuItem>
            </Menu>
        </DockPanel>

        <Grid>

            <TreeView Name="treeview" Grid.Row="0">
                
                <TreeView.ItemTemplate>
                    
                    <HierarchicalDataTemplate DataType="{x:Type local:Task}" ItemsSource="{Binding Items}">
                        <StackPanel Orientation="Horizontal">
                            <Label>Task Name</Label>
                            <TextBox Width="120" VerticalAlignment="Center" Text="Text Goes here"></TextBox>
                            <ProgressBar Width="200" Height="10" Value="{Binding Path=GetProgressPercentage}" Margin="4"></ProgressBar>
                            <Label>Hours Total</Label>
                            <TextBox Width="30" VerticalAlignment="Center" Text="{Binding Path=HrsTotal, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                            </TextBox>
                            <Label>Hours Remaining</Label>
                            <TextBox Width="30" VerticalAlignment="Center" Text="{Binding HrsRemaining, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></TextBox>
                            <Button Margin="4" Width="20" Command="{Binding Path=AddBtnClick}" >+</Button>
                            <Button Margin="4" Width="20" Command="{Binding Path=DelBtnClick}" >-</Button>
                        </StackPanel>
                    </HierarchicalDataTemplate>
                    
                </TreeView.ItemTemplate>
                
                <TreeView.ItemContainerStyle>
                    <Style TargetType="TreeViewItem">
                        <Setter Property="IsExpanded" Value="{Binding IsExpanded}"/>
                    </Style>
                </TreeView.ItemContainerStyle>
                
            </TreeView>

        </Grid>
    </StackPanel>
</Window>

Task.cs

class Task : TreeViewItemBase
    {

        public Task()
        {
            this.Items = new ObservableCollection<Task>();
        }

        public ObservableCollection<Task> Items { get; set; }
        public ObservableCollection<Task> ParentItems { get; set; }

        private int _hrsTotal;
        private int _hrsRemaining;

        public int HrsTotal
        {
            get { return _hrsTotal; }
            set
            {
                if (_hrsTotal != value)
                {
                    _hrsTotal = value;
                    this.NotifyPropertyChanged("HrsTotal");
                }
            }
        }
        public int HrsRemaining
        {
            get { return _hrsRemaining; }
            set
            {
                if (_hrsRemaining != value)
                {
                    _hrsRemaining = value;
                    this.NotifyPropertyChanged("HrsRemaining");
                }
            }
        }

        public int GetProgressPercentage()
        {
            if(this.HrsTotal != 0)
            {
                return 100 -((this.HrsRemaining / this.HrsTotal) * 100);
            }
            else { return 0; }
            
        }


        private ICommand _addBtnClick;
        private ICommand _delBtnClick;

        public ICommand AddBtnClick
        {
            get
            {
                if(_addBtnClick == null)
                {
                    _addBtnClick = new RelayCommand(param => this.AddNewTask(), param => this.CanAddTask());
                }
                return _addBtnClick;
            }
        }

        public ICommand DelBtnClick
        {
            get
            {
                if(_delBtnClick == null)
                {
                    _delBtnClick = new RelayCommand(param => this.DeleteTask(), param => this.CanDelTask());
                }
                return _delBtnClick;
            }
        }

        private bool CanAddTask()
        {
            return true;
        }

        private bool CanDelTask()
        {
            if(ParentItems != null) { return true; }
            else { return false; }
        }

        public void AddNewTask()
        {
            Task newTask = new Task();
            newTask.ParentItems = this.Items;
            this.Items.Add(newTask);
            this.IsExpanded = true;

        }

        public void DeleteTask()
        {
            ParentItems.Remove(this);
        }
    }

TreeViewItemBase.cs

    class TreeViewItemBase : INotifyPropertyChanged
    {
        private bool isExpanded;
        public bool IsExpanded
        {
            get { return this.isExpanded; }
            set
            {
                if(value != this.isExpanded)
                {
                    this.isExpanded = value;
                    NotifyPropertyChanged("IsExpanded");
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void NotifyPropertyChanged(string propName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
            }
        }
    }

RelayCommand.cs

class RelayCommand : ICommand
    {
        readonly Action<object> _execute;
        readonly Predicate<object> _canExecute;

        public RelayCommand(Action<object> execute) : this(execute, null) { }
        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if(execute == null)
            {
                throw new ArgumentNullException("execute");   
            }
            _execute = execute; _canExecute = canExecute;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null ? true : _canExecute(parameter);
        }
        public void Execute(object parameter)
        {
            _execute(parameter);
        }
    }

I am expecting to put in a number for the 'Total hours' Textbox then put in a number for the 'Hours remaining' Textbox and the see the percentage bar reflect the progress.


Solution

  • You do not bind to methods. You bind to properties. When properties raise the INotifyPropertyChanged.PropertyChanged event the binding gets updated automatically.

    you could use:

    public int ProgressPercentage
    {
            get
            {
                if(this.HrsTotal != 0)
                {
                    return 100 -((this.HrsRemaining / this.HrsTotal) * 100);
                }
                else { return 0; }
                
            }
    }
    

    and call OnPropertyChanged(nameof(ProgressPercentage)) when HrsRemaining or HrsTotal have changed.