Home > Software engineering >  Summing class attributes in XAML ListView control
Summing class attributes in XAML ListView control

Time:03-02

I'm pretty new to C# so I've been struggling with this for so long I dont know what i should do...

I want to sum 2 (or more) values from my class (sum interestRate_1) among other calculations and display the results in my respective ListView columns for every class instance I create to my ObservableCollections -list. The ListView itself is bound by ItemSource and contains all the created Cust -objects im trying to perform calculations on.

I'm totally new to XAML so i dont know how it works. People tell me to use a converter but I'm not sure what it implies...

Here's what I've tried:

XAML declaration:

<Window x:Class="MyProject.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:MyProject"
    mc:Ignorable="d"
    Title="MyProject (beta)" Height="450" Width="1000" MinWidth="1000" MinHeight="450" ResizeMode="CanResize">

XAML ListView:

<ListView x:Name="ListTest" Margin="10,150,10,66" Background="#FF0077B6" Foreground="White" MouseDoubleClick="ListTest_MouseDoubleClick" SelectionChanged="ListTest_SelectionChanged" IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding CustList}" Grid.RowSpan="2">
    <ListView.Effect>
        <DropShadowEffect/>
    </ListView.Effect>
    <ListView.View>
        <GridView>

            <GridViewColumn Header="Name" Width="136" DisplayMemberBinding="{Binding Name}"/>
            <GridViewColumn Header="Amount (€)" Width="80" DisplayMemberBinding="{Binding Sum}"/>
            <GridViewColumn Header="Amount With Interest (€)"  Width="80">
                <MultiBinding Converter="{StaticResource AddConverter}">
                    <Binding Path="Sum"/>
                    <Binding Path="interestRate_1"/>
                </MultiBinding>
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>

C# Converter List of Cust -objects:

namespace MyProject
{
    public class AddConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type tagetType, object parameter, CultureInfo culture)
        {

            var decimalValues = values.Cast<decimal>().ToArray();
            var resultSum = decimalValues.Sum().ToString();
            return resultSum;

        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    public partial class MainWindow : Window
    {

        public ObservableCollection<Cust> CustList = new ObservableCollection<Cust>();

        public MainWindow()
        {
            InitializeComponent();

            ListTest.ItemsSource = CustList;
            
        }

//...

Relevant Class attributes / Properties:

public class Cust
{
    public Name {get; set;}
    private decimal sum;
    public decimal Sum
    {

        get { return sum; }
        set { sum = value; }

    }

    public decimal interestRate_1 { get; set; } = 3.0M;
    public decimal interestRate_2 { get; set; } = 5.0M;

// ...

Wha i know is that XAML cannot find the converter from the .cs file and that I use Header twice in the column I'm trying to convert (which I dont understand). Am I totally lost here?

CodePudding user response:

Welcome to the land of xaml and c# :)

The Key Concept of WPF or XAML and C# is MVVM which stands for Model View ViewModel In your example the Cust class would be the Model the XAML with the code behind would be the view. What is missing is the ViewModel. It's the Connection between your Code and your view.

Here a a view things that stick out to me

  1. you should look up the concept of MVVM and bindings:
  • the cust would be the model
  1. A Converter is usally used to bind a value e.G.
  • if(value == null) return Visibility.Collapsed So i don't think it's the right tool for the job here.
  1. You may consider using the DataGrid Control instead of the GridView inside of an listView
  2. If you want to use a ListView you have to search for Templating (ItemTemplates in particular)
  3. If you want to change your values and update your view u have to look up INotifyPropertyChanged or find a basic implementation to set you properties.

But anyway here is a solution to your problem:

<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <DataGrid ItemsSource="{Binding Custs}"/>
        <Label Content="Result"/>
        <Label Content="{Binding ResultSum}"/>
    </StackPanel>
</Window>

So here is an basic example of a view model

public class MainWindowViewModel
    {
        public ObservableCollection<Cust> Custs { get; set; }

        public decimal ResultSum { get; set; }

        public MainWindowViewModel()
        {
            Custs = new ObservableCollection<Cust>
            {
                new Cust
                {
                    Name = "Joe",
                    Sum = 1.2M,
                },
                new Cust
                {
                    Name = "Jane",
                    Sum = 3.2M,
                },
            };

            ResultSum = Custs.Sum(cust => cust.Sum);
        }
    }

And how to use it

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            DataContext = new MainWindowViewModel();
        }
    }

And your Cust class

public class Cust
    {
        public string Name
        {
            get;
            set;
        }
        private decimal sum;

        public decimal Sum
        {

            get { return sum; }
            set { sum = value; }

        }

        public decimal interestRate_1 { get; set; } = 3.0M;
        public decimal interestRate_2 { get; set; } = 5.0M;
    }

The result looks like this

CodePudding user response:

The converter is not found, because there is no instance of it. Create an instance in any resource dictionary in XAML, e.g. the application resources or locally in the ListView.Resources.

<ListView.Resources>
   <local:AddConverter x:Key="AddConverter"/>
</ListView.Resources>

The binding of ItemsSource is redandant, as you already assign it in code, use or the other.

ItemsSource="{Binding CustList}"
ListTest.ItemsSource = CustList;

However, the ItemSource binding will only work, if you set the data context correctly, e.g.:

public MainWindow()
{
    InitializeComponent();
    DataContext = this;
}

Or in XAML markup like this.

<Window x:Class="MyProject.MainWindow"
    ...
    DataContext="{Binding RelativeSource={RelativeSource Self}}">

Finally, it says you assign the header twice, because the direct content of the GridViewColumn assigns the Header property, too. Use the DisplayMemberBinding property explicitly instead.

<GridViewColumn Header="Amount With Interest (€)"  Width="80">
   <GridViewColumn.DisplayMemberBinding>
      <MultiBinding Converter="{StaticResource AddConverter}">
         <Binding Path="Sum"/>
         <Binding Path="interestRate_1"/>
      </MultiBinding>
   </GridViewColumn.DisplayMemberBinding>
</GridViewColumn>

Here is the complete code for your ListView.

<ListView x:Name="ListTest" Margin="10,150,10,66" Background="#FF0077B6" Foreground="White" MouseDoubleClick="ListTest_MouseDoubleClick" SelectionChanged="ListTest_SelectionChanged" IsSynchronizedWithCurrentItem="True" Grid.RowSpan="2">
   <ListView.Resources>
      <local:AddConverter x:Key="AddConverter"/>
   </ListView.Resources>
   <ListView.Effect>
      <DropShadowEffect/>
   </ListView.Effect>
   <ListView.View>
      <GridView>
         <GridViewColumn Header="Name" Width="136" DisplayMemberBinding="{Binding Name}"/>
         <GridViewColumn Header="Amount (€)" Width="80" DisplayMemberBinding="{Binding Sum}"/>
         <GridViewColumn Header="Amount With Interest (€)"  Width="80">
            <GridViewColumn.DisplayMemberBinding>
               <MultiBinding Converter="{StaticResource AddConverter}">
                  <Binding Path="Sum"/>
                  <Binding Path="interestRate_1"/>
               </MultiBinding>
            </GridViewColumn.DisplayMemberBinding>
         </GridViewColumn>
      </GridView>
   </ListView.View>
</ListView>

You do not necessarily need a value converter. You could also expose a computed property in Cust and bind it instead. It depends on your requirements.

public class Cust
{
   // ...your other code.

   public decimal AmountWithInterest => sum   interestRate_1;
}
<GridViewColumn Header="Amount With Interest (€)"  Width="80" DisplayMemberBinding="{Binding AmountWithInterest}"/>

Please also be aware that you do not implement INotifyPropertyChanged, which means that changes to your properties and the computed property will not be reflected in the user interface.


Apart from the errors you get, you should probably get familiar with the MVVM pattern.

  • Related