I have an ItemsControl that is bound to a list of elements. How can I have it display only a segment of that list, say, based on a given Offset and Count ?
Obviously I could declare an IEnumerable property as MyList.Skip(Offset).Take(Count), use that in the binding and notify PropertyChanged each time Offset or Count are modified. That works fine, except that each time the ItemsControl would recreate the views for all of its items, even for those that aren't new.
I guess a more idiomatic way would be to have a CollectionView but it doesn't seem to support the concept of current segment. I tried to use an index-based filter but besides from feeling wrong, it suffers from the same performance problem as before.
So the only way I saw so far was to maintain an ObservableCollection with the correct list elements and use this in the binding, but shouldn't there be an easier way?
Here's my test program (I know the code for MyObservableCollection does not work in the general case)
MainWindow.xaml
<Window x:Class="ItemsControlSegmentView.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ItemsControlSegmentView"
x:Name="This"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<Style TargetType="ItemsControl">
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding}" Background="{Binding DataContext.CurrentColor, ElementName=This, Mode=OneTime}" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel>
<StackPanel Orientation="Horizontal">
<ItemsControl ItemsSource="{Binding MyEnumerable}"/>
<ItemsControl ItemsSource="{Binding MyCollectionView}"/>
<ItemsControl ItemsSource="{Binding MyObservableCollection}"/>
</StackPanel>
<Slider Maximum="{Binding MyList.Count}" Value="{Binding Offset}"/>
<Slider Maximum="{Binding MaxCount}" Value="{Binding Count}"/>
</StackPanel>
</Window>
MainWindowViewModel
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Data;
using JetBrains.Annotations;
namespace ItemsControlSegmentView
{
public class MainWindowViewModel : INotifyPropertyChanged
{
private int offset;
private int count = 7;
private readonly Random random = new Random();
public string CurrentColor { get; private set; } = "White";
public int Offset
{
get { return offset; }
set
{
if (offset == value) return;
var atEnd = value > offset;
offset = value;
UpdateAllViews(atEnd);
OnPropertyChanged(nameof(MaxCount));
OnPropertyChanged();
}
}
public int Count
{
get { return count; }
set
{
if (count == value) return;
count = value;
UpdateAllViews(true);
OnPropertyChanged();
}
}
public int MaxCount => MyList.Count - Offset;
public List<string> MyList { get; } = new List<string>(Enum.GetNames(typeof(DayOfWeek)));
private void UpdateAllViews(bool atEnd)
{
CurrentColor = $"#{random.Next():X8}";
UpdateMyEnumerable();
UpdateMyCollectionView();
UpdateMyObservableCollection(atEnd);
}
public IEnumerable<string> MyEnumerable => MyList.Skip(Offset).Take(Count);
private void UpdateMyEnumerable() => OnPropertyChanged(nameof(MyEnumerable));
public ListCollectionView MyCollectionView { get; }
private void UpdateMyCollectionView() => MyCollectionView.Filter = entry =>
{
var indexOf = MyList.IndexOf((string)entry);
return indexOf >= Offset && indexOf < Offset + Count;
};
public ObservableCollection<string> MyObservableCollection { get; }
private void UpdateMyObservableCollection(bool atEnd)
{
foreach (var s in MyList.Except(MyEnumerable))
MyObservableCollection.Remove(s);
var i = 0;
foreach (var s in MyEnumerable.Except(MyObservableCollection))
if (atEnd) MyObservableCollection.Add(s);
else MyObservableCollection.Insert(i++, s);
}
public MainWindowViewModel()
{
MyCollectionView = new ListCollectionView(MyList);
MyObservableCollection = new ObservableCollection<string>(MyList);
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
This is how the window looks like after having scrolled the offset slider to the right end and back, step by step. Only the third ItemsControl keeps its existing entries:
Related
I’m learning C# and building a UI that reads and writes integers to an XML config file. The UI uses a variety of custom user controls. I have a 3 radiobutton user control that binds to a single int variable (control returns 0,1,2). The control uses an event to trigger the update. It looks at the 3 isChecked properties to determine the new int value. But I don’t know how to update the original binding value from the code behind. It's once removed so to speak because there are two binds..one in the main window and one in the user control. As a beginner am lost at this point. BTW reading the int value into the 3 radiobuttons is working using a converter.
here is the user control xaml.cs...
namespace btsRV7config.controls
{
public partial class ui3X : UserControl
{
public ui3X()
{
InitializeComponent();
}
void _event(object sender, RoutedEventArgs e)
{
int newValue = 0;
if (rdbttn1.IsChecked == true) { newValue = 0; }
else if (rdbttn2.IsChecked == true) { newValue = 1; }
else if (rdbttn3.IsChecked == true) { newValue = 2; }
txtb.Text = newValue.ToString(); //tempRemove
// !!! assign newValue to Binding Source !!!
//---------------------------------------------
uiBinding1 = newValue;
BindingOperations.GetBindingExpression(rdbttn1, RadioButton.IsCheckedProperty).UpdateSource();
//---------------------------------------------
}
public static readonly DependencyProperty uiBinding1Property = DependencyProperty.Register("uiBinding1", typeof(int), typeof(ui3X));
public int uiBinding1
{
get { return (int)GetValue(uiBinding1Property); }
set { SetValue(uiBinding1Property, value); }
}
public static readonly DependencyProperty uiBinding2Property = DependencyProperty.Register("uiBinding2", typeof(int), typeof(ui3X));
public int uiBinding2
{
get { return (int)GetValue(uiBinding2Property); }
set { SetValue(uiBinding2Property, value); }
}
public static readonly DependencyProperty uiBinding3Property = DependencyProperty.Register("uiBinding3", typeof(int), typeof(ui3X));
public int uiBinding3
{
get { return (int)GetValue(uiBinding3Property); }
set { SetValue(uiBinding3Property, value); }
}
}
}
here is user control xaml
<UserControl x:Class="btsRV7config.controls.ui3X"
x:Name="root"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Horizontal" Height="22">
<RadioButton Name="rdbttn1" VerticalAlignment="Center" Margin="0 0 10 0"
IsChecked="{Binding ElementName=root, Path=uiBinding1}"
Click="_event" />
<RadioButton Name="rdbttn2" VerticalAlignment="Center" Margin="0 0 10 0"
IsChecked="{Binding ElementName=root, Path=uiBinding2}"
Click="_event" />
<RadioButton Name="rdbttn3" VerticalAlignment="Center"
IsChecked="{Binding ElementName=root, Path=uiBinding3}"
Click="_event" />
<TextBox Name="txtb" Margin="5 0 0 0" Width="20" Height="17" /> <!-- tempRemove -->
</StackPanel>
</UserControl>
here is an example of the user control used in MainWindow.xaml
<Window x:Class="btsRV7config.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:btsRV7config.controls"
xmlns:converter="clr-namespace:btsRV7config.converters"
Title="Vans RV7 Configuration" Height="350" Width="525" >
<Window.Resources>
<converter:Int_Int_Bool_Converter x:Key="Int_Int_Bool" />
</Window.Resources>
<Grid>
<controls:ui3X uiName="Font Color" ui1="Black" ui2="Green" ui3="Cyan"
uiBinding1="{Binding RV7sld_DFfontColor, Converter={StaticResource Int_Int_Bool}, ConverterParameter=0}"
uiBinding2="{Binding RV7sld_DFfontColor, Converter={StaticResource Int_Int_Bool}, ConverterParameter=1}"
uiBinding3="{Binding RV7sld_DFfontColor, Converter={StaticResource Int_Int_Bool}, ConverterParameter=2}" />
</Grid>
</Window>
here is MainWindow.xaml.cs
namespace btsRV7config
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
record data = new record();
DataContext = data;
}
}
public class record : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _RV7sld_DFfontColor = RV7sld_dict["DFfontColor"];
public int RV7sld_DFfontColor
{
get
{ return _RV7sld_DFfontColor; }
set
{
_RV7sld_DFfontColor = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("RV7sld_DFfontColor"));
}
}
}
}
}
Sorry for posting so much code - I think the important is the user controls xaml.cs at top.
here is a link to a picture of the UI.
I've simplified the code I've posted to fit.
http://www.baytower.ca/photo/uiSample.jpg
So - 'Font Color'(RV7sld_DFfontColor) can be black(0) green(1) cyan(2)
Danny
The BindingOperations class enables you to "force" the binding updates in either direction.
Let's say there is a string property X in the code behind and there is a TextBox in XAML, bound to that property:
// C#:
public string X { get; set; }
// XAML:
<TextBox Name="textBox1" Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=my:MainWindow, AncestorLevel=1}, Path=X}" />
To copy from textBox1.Text to X do the following:
BindingOperations.GetBindingExpression(textBox1, TextBox.TextProperty).UpdateSource();
To copy from X to textBox1.Text do the following:
BindingOperations.GetBindingExpression(textBox1, TextBox.TextProperty).UpdateTarget();
I have this working stub:
XAML
<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">
<DataGrid ItemsSource="{Binding MyList}" AutoGenerateColumns="False" CanUserAddRows="False" GridLinesVisibility="None" HeadersVisibility="None" BorderThickness="0" ScrollViewer.VerticalScrollBarVisibility="Visible">
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Label>
<TextBlock Text="{Binding Id}" />
</Label>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Label>
<TextBlock Text="{Binding TheData}" />
</Label>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Window>
Code-behind
using System.Windows;
using WpfApp1.ViewModels;
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
DataContext = new MainWindowVM();
InitializeComponent();
}
}
}
ViewModel
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace WpfApp1.ViewModels
{
public class MainWindowVM
{
Random random = new Random();
public List<ListItemVM> MyList { get; set; } = new List<ListItemVM>
{
new ListItemVM(1, "Data1"),
new ListItemVM(2, "Data2"),
new ListItemVM(3, "Data3"),
};
public MainWindowVM()
{
// Start an infinite task that updates the data every 2 second
// This emulates an external process that sends new data that must be displayed
Task.Factory.StartNew(() =>
{
while (true)
{
Task.Delay(2000).Wait();
var nextData = new string(Enumerable.Repeat("ABCDEFG", 10).Select(s => s[random.Next(s.Length)]).ToArray());
MyList[1].SetNewData(nextData);
}
});
}
}
}
Item ViewModel
using System.ComponentModel;
namespace WpfApp1.ViewModels
{
public class ListItemVM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public int Id { get; set; }
public string TheData { get; set; }
public ListItemVM(int id, string theData)
{
Id = id;
TheData = theData;
}
internal void SetNewData(string nextData)
{
// Change data
TheData = nextData;
// Notify the UI of the change
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TheData)));
}
}
}
Every 2 seconds, I see the data update in the UI for the second item in the DataGrid.
The question
I would like the DataGridRow to get highlighted and fade out in 1 second on every update. Can someone help me achieve that please?
You could create an attached behaviour that performs the animation:
public static class Animator
{
private static readonly HashSet _rows = new HashSet();
public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached("Value", typeof(object),
typeof(Animator), new PropertyMetadata(new PropertyChangedCallback(OnValuePropertyChanged)));
public static object GetValue(DataGridRow d) => d.GetValue(ValueProperty);
public static void SetValue(DataGridRow d, object value) => d.SetValue(ValueProperty, value);
private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DataGridRow row = (DataGridRow)d;
if (!_rows.Contains(row))
{
_rows.Add(row);
row.Unloaded += Row_Unloaded;
}
else
{
ColorAnimation animation = new ColorAnimation();
animation.From = Colors.Gray;
animation.To = Colors.White;
animation.Duration = new Duration(TimeSpan.FromSeconds(1));
Storyboard.SetTarget(animation, row);
Storyboard.SetTargetProperty(animation, new PropertyPath("Background.Color"));
Storyboard sb = new Storyboard();
sb.Children.Add(animation);
sb.Begin();
}
}
private static void Row_Unloaded(object sender, RoutedEventArgs e)
{
DataGridRow row = (DataGridRow)sender;
_rows.Remove(row);
row.Unloaded -= Row_Unloaded;
}
}
Usage:
<DataGrid.ItemContainerStyle>
<Style TargetType="DataGridRow">
<Setter Property="local:Animator.Value" Value="{Binding TheData}" />
</Style>
</DataGrid.ItemContainerStyle>
You'll need to do something like:
Set DataGrid.RowStyle to a new Style - probably override the default ControlTemplate (see this answer) to add a background element you can "highlight"
Define an appropriate Trigger in Style.Triggers - maybe a DataTrigger based on TheData not being empty
Use the trigger to start an animation - see msdn documentation
You might need to do something to defer the application of this style - presumably you don't want all the rows to highlight on initial load. To do this you could look at using a Xaml behavior to do set the RowStyle after Loaded has fired
I'm binding a ListView to an ICollectionView in my viewmodel. The ICollectionView has some predefined filters that are applied when you click some buttons. However I cannot seem to find any way to (auto) select the first item in the ListView after the collection has been filtered.
I've tried to set SelectedIndex=0, add both Target and Source notification to the binding, but all are ineffective when the filter applies.
Any pointers on how to achieve this?
EDIT: Below code illustrates my issue I'd say.
XAML:
<Window x:Class="CollectionViewTest.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:CollectionViewTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- MENU -->
<StackPanel Orientation="Vertical">
<Button Content="Numbers below 4" Click="Below4_Click" Width="100"/>
<Button Content="Numbers below 7" Click="Below7_Click" Width="100"/>
<Button Content="All numbers" Click="All_Click" Width="100"/>
</StackPanel>
<!-- LIST -->
<ListView
Grid.Column="1"
SelectedIndex="0"
ItemsSource="{Binding Numbers, Mode=OneWay}"
SelectedItem="{Binding SelectedNumber, Mode=TwoWay}">
<ListView.Resources>
<DataTemplate DataType="{x:Type local:Number}">
<TextBlock Text="{Binding Value}" />
</DataTemplate>
</ListView.Resources>
</ListView>
<!-- DETAILS -->
<TextBlock Grid.Column="2" Text="{Binding SelectedNumber.Text}" Width="100"/>
</Grid>
</Window>
Code-Behind:
using System.Windows;
namespace CollectionViewTest
{
public partial class MainWindow : Window
{
private MainViewModel vm;
public MainWindow()
{
InitializeComponent();
vm = (MainViewModel)DataContext;
}
private void Below4_Click(object sender, RoutedEventArgs e)
{
vm.MenuFilter = f => f.Value < 4;
}
private void Below7_Click(object sender, RoutedEventArgs e)
{
vm.MenuFilter = f => f.Value < 7;
}
private void All_Click(object sender, RoutedEventArgs e)
{
vm.MenuFilter = f => true;
}
}
}
ViewModel:
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Data;
using System.Collections.ObjectModel;
namespace CollectionViewTest
{
public class MainViewModel : PropertyChangedBase
{
public MainViewModel()
{
Numbers = new ObservableCollection<Number>();
NumberCollection = CollectionViewSource.GetDefaultView(Numbers);
NumberCollection.Filter = Filter;
NumberCollection.SortDescriptions.Add(new SortDescription("Value", ListSortDirection.Ascending));
for (int i = 0; i < 10; i++)
Numbers.Add(new Number { Value = i, Text = $"This is number {i}." });
}
private Func<Number, bool> menuFilter;
public Func<Number, bool> MenuFilter
{
get => menuFilter;
set
{
menuFilter = value;
NumberCollection.Refresh();
}
}
private bool Filter(object item)
{
var number = (Number)item;
return MenuFilter == null ? true : MenuFilter(number);
}
public ObservableCollection<Number> Numbers { get; set; }
public ICollectionView NumberCollection { get; set; }
private Number selectedNumber;
public Number SelectedNumber { get => selectedNumber; set => Set(ref selectedNumber, value); }
}
public class Number : PropertyChangedBase
{
public int Value { get; set; }
private string text;
public string Text { get => text; set => Set(ref text, value); }
}
public class PropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void Set<T>(ref T field, T newValue = default(T), [CallerMemberName] string propertyName = null)
{
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
As you can see, pressing one of the buttons changes the Filter and calls Refresh on the collection. What I would like to have, is that the first item in the list (here '0') is selected automatically which then would display the text "This is number 0" in the text in column 2.
I have tried both the SelectedIndex=0 and also MoveCurrentToFirst but nothing is selected.
Don't set SelectedIndex when binding to an ICollectionView. Instead, set its CurrentItem via MoveCurrentTo() or MoveCurrentToFirst():
myCollectionView.MoveCurrentTo(someItem);
...
myCollectionView.MoveCurrentToFirst();
Also, set IsSynchronizedWithCurrentItem on your ListView:
<ListView IsSynchronizedWithCurrentItem="True" ...
Detect when filter is applied
When the filter is evaluated, the collection view is refreshed which in turn resets the collection. To detect this, listen for the CollectionChanged event and look for the NotifyCollectionChangedAction.Reset flag. Please refer to the CollectionView source code for more details.
I used to just create a block of text by converting a list of strings to one string with newlines. This Binding worked; updated when it was supposed to and all, but I'm trying to move the list of text into an ItemsControl as they will need to be hyperlinks at some point in the future. Problem: The ItemsControl does not change when the PropertyChangeEvent is fired. The Relevant Code is as follows:
Xaml
<local:BaseUserControl x:Class="BAC.Windows.UI.Views.ErrorsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:BAC.Windows.UI.Views"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
...
<ItemsControl ItemsSource="{Binding Path=ErrorMessages}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"></TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!--<TextBlock VerticalAlignment="Center" Visibility="{Binding ErrorMessages, Converter={StaticResource VisibleWhenNotEmptyConverter}}" Text="{Binding ErrorMessages, Converter={StaticResource ErrorMessagesToTextConverter}}">
(What I used to use)
</TextBlock>-->
...
</local:BaseUserControl>
ViewModel
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using ASI.Core.Core;
using ASI.Core.DTO;
using ASI.Core.Extensions;
using ASI.Core.Mappers;
using BAC.Core.Resources;
using BAC.Core.Services;
using BAC.Core.ViewModels.Views;
namespace BAC.Core.ViewModels
{
public interface IErrorsViewModel : IViewModel<IErrorsView>
{
}
public class ErrorsViewModel : BaseViewModel<IErrorsView>, IErrorsViewModel
{
...
private readonly ErrorDTO _errorDTO;
private readonly ErrorDTO _warningDTO;
public ErrorsViewModel(...) : base(view)
{
...
//Just added this string to know that it's at least binding. This Message displays, and never changes.
ErrorMessages = new List<string>() {"Simple Message"};
//Tells the View to bind dataContext to Viewmodel
Edit();
}
private void errorDTOOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
ErrorDTO dto;
if (!string.Equals(propertyChangedEventArgs.PropertyName, nameof(dto.HasError))) return;
ErrorMessages.Clear();
_errorDTO.ErrorMessages.Each(x => ErrorMessages.Add(Constants.Captions.Errors + ": " + x));
_warningDTO.ErrorMessages.Each(x => ErrorMessages.Add(Constants.Captions.Warnings + ": " + x));
OnPropertyChanged(() => ErrorMessages);
OnPropertyChanged(() => HasError);
OnPropertyChanged(() => HasWarning);
}
...
public bool HasError => _errorDTO.HasError;
public bool HasWarning => _warningDTO.HasError;
public IList<string> ErrorMessages { get; set; }
...
}
And just because I know people may ask to see it...
public class BaseNotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
{
var body = propertyExpression.Body as MemberExpression;
if (body != null)
OnPropertyChanged(body.Member.Name);
}
protected void OnEvent(Action action)
{
try
{
action();
}
catch
{ }
}
}
I'm sure it's something stupidy simple I'm doing, but the harder I look, the more I get frusterated by what should something simple. Why does the binding work for all other conrols except ItemSource? What's so special about it?
I'll also add anotehr explanation (Even though I know this is old).
The reason this will not update the property is that the List object is not actually changing, so the ListView will not update the list. The only way to do this without using "ObservableCollection" is to create a brand new list on each property change like so:
private void errorDTOOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
if (!string.Equals(propertyChangedEventArgs.PropertyName, nameof(dto.HasError))) return;
OnPropertyChanged(() => ErrorMessages);
}
public List<string> ErrorMessages => getErrorMessages();
private List<string> getErrorMessages() {
//create list in a manner of your choosing
}
Hopefully that helps people when they run into this.
So I was able to get your code to work by using an ObservableCollection instead of the List. The ObservableCollection generates a list changed notification automatically when its collection is changed. Below is my sample code. I use a timer to update the error list every second.
<Window x:Class="TestEer.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:TestEer"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<ItemsControl ItemsSource="{Binding Path=ErrorMessages}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
using System.Collections.ObjectModel;
using System.Timers;
using System.Windows;
using System.Windows.Data;
namespace TestEer
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private Timer _timer;
private readonly object _sync = new object( );
public MainWindow( )
{
InitializeComponent( );
BindingOperations.EnableCollectionSynchronization( ErrorMessages, _sync );
_timer = new Timer
{
AutoReset = true,
Interval = 1000
};
_timer.Elapsed += _timer_Elapsed;
_timer.Enabled = true;
_timer.Start( );
}
private void _timer_Elapsed( object sender, ElapsedEventArgs e )
{
ErrorMessages.Add( $"Error # {e.SignalTime}" );
}
public ObservableCollection<string> ErrorMessages { get; } = new ObservableCollection<string>( );
}
}
We set up the OnPropertyChanged() method in the get set methods before the constructor and this seemed to work!
private bool _theString;
public bool TheString
{
get { return _theString; }
set { _theString = value; OnPropertyChanged(); }
}
Use {Binding TheString} in your .xaml.
Hope this helps!
My problem: Id like to , after two combobox variables are selected, to divide these two and set the Textbox to the result of the calculation.
The two Comboboxes: Körpergröße & Gewicht
The textbox: BMI
First of all, the code im using ( which apparently isnt working now)
private void fillTextBox(float value1, float value2)
{
BMI.Text = (value1 / value2).ToString();
}
private void Körpergröße_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
float a;
float b;
//In this way you can compare the value and if it is possible to convert into an integer.
if (float.TryParse(Körpergröße.SelectedItem.ToString(), out a) && float.TryParse(Gewicht.SelectedItem.ToString(), out b))
{
fillTextBox(a, b);
}
}
private void Gewicht_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
float a;
float b;
//In this way you can compare the value and if it is possible to convert into an integer.
if (float.TryParse(Körpergröße.SelectedItem.ToString(), out a) && float.TryParse(Gewicht.SelectedItem.ToString(), out b))
{
fillTextBox(a, b);
}
}
The default values of the two comboboxes are strings.. ("Bitte auswählen")
Pictures how it looks like now. After two int values are selected, the result of the calculation should appear in the BMI Textbox, but its still blank. it looks like the ToString() Methods do not save into a, and neither into b..therefore the values cant be used in the fillTextBox method
It would be nice if someone could answer me with a code with some sidenotes, in order for me to understand..
Thanks in advance!
Here's an example of how you would write a trivial BMI calculator in WPF. This is the way XAML is intended to be used: All of the logic is in the View Model class, BMIViewModel. The View (XAML file) wraps a UI around that logic. The codebehind file is only used if you need to provide some special logic unique to the view itself. Very often it is not used at all. Here, it's not used.
This is very different from what you may be used to and it's a steep learning curve in a lot of ways, but I've learned to like it very much. If you break up program logic into sensible chunks in various View Models, you can present those View Models in the UI in various differing ways. You get tremendous freedom and power. Once you've got a debugged and tested View Model, you can write new UI for it without touching the program logic at all.
If you study and understand this code, you will have a rudimentary but solid basis to start learning XAML. The bindings are important, and OnPropertyChanged is very important: That notification is how the view model lets the bindings know that a value has changed.
I don't like writing this much code for a beginner, but your code (which is not your code -- it's entirely copied from bad answers to previous iterations of this question), is unusable.
XAML:
<Window
x:Class="TestApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestApplication"
Title="MainWindow" Height="350" Width="525"
>
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<StackPanel Orientation="Vertical">
<StackPanel.Resources>
<Style x:Key="FieldLabel" TargetType="Label">
<Setter Property="Width" Value="120" />
</Style>
<Style TargetType="ComboBox">
<Setter Property="Width" Value="140" />
</Style>
<Style TargetType="TextBox">
<Setter Property="Width" Value="140" />
</Style>
</StackPanel.Resources>
<StackPanel Orientation="Horizontal">
<Label Content="Height" Style="{StaticResource FieldLabel}" />
<ComboBox
ItemsSource="{Binding Heights}"
DisplayMemberPath="Name"
SelectedValuePath="Value"
SelectedValue="{Binding Height}"
/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="Weight" Style="{StaticResource FieldLabel}" />
<ComboBox
ItemsSource="{Binding Weights}"
DisplayMemberPath="Name"
SelectedValuePath="Value"
SelectedValue="{Binding Weight}"
/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="BMI" Style="{StaticResource FieldLabel}" />
<TextBox IsReadOnly="True" Text="{Binding BMI}" />
</StackPanel>
</StackPanel>
</Grid>
</Window>
C# code behind (I added no code at all here):
using System;
using System.Windows;
namespace TestApplication
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
BMIViewModel.cs:
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace TestApplication
{
public class BMIListItem
{
public BMIListItem(string name, float value)
{
Name = name;
Value = value;
}
public BMIListItem(float value)
{
Name = value.ToString();
Value = value;
}
public String Name { get; set; }
public float Value { get; set; }
}
public class BMIViewModel : INotifyPropertyChanged
{
public BMIViewModel()
{
// Of course you would write loops for these in real life.
// You should not need help with that.
Heights = new List<BMIListItem>
{
new BMIListItem("Bitte auswählen", float.NaN),
new BMIListItem("Dummy", 0),
new BMIListItem(150),
new BMIListItem(151),
new BMIListItem(152),
// etc.
};
Weights = new List<BMIListItem>
{
new BMIListItem("Bitte auswählen", float.NaN),
new BMIListItem("Dummy", 0),
new BMIListItem(40),
new BMIListItem(41),
new BMIListItem(42),
// etc.
};
}
public List<BMIListItem> Heights { get; private set; }
public List<BMIListItem> Weights { get; private set; }
#region BMI Property
private float _bmi = 0;
public float BMI
{
get { return _bmi; }
set
{
if (value != _bmi)
{
_bmi = value;
OnPropertyChanged("BMI");
}
}
}
#endregion BMI Property
#region Height Property
private float _height = float.NaN;
public float Height
{
get { return _height; }
set
{
if (value != _height)
{
_height = value;
UpdateBMI();
OnPropertyChanged("Height");
}
}
}
#endregion Height Property
#region Weight Property
private float _weight = float.NaN;
public float Weight
{
get { return _weight; }
set
{
if (value != _weight)
{
_weight = value;
UpdateBMI();
OnPropertyChanged("Weight");
}
}
}
#endregion Weight Property
private void UpdateBMI()
{
if (float.IsNaN(Weight) || float.IsNaN(Height) || Height == 0)
{
BMI = 0;
}
else
{
BMI = Weight / Height;
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(String propName)
{
var handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propName));
}
}
#endregion INotifyPropertyChanged
}
}