Updating control property breaks OneWay binding? - c#

Consider this very simple example where I have a UserControl like this:
UserControl XAML:
<UserControl x:Class="BindingTest.SomeControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="SomeControlElement">
<Grid>
<TextBlock Text="{Binding ElementName=SomeControlElement, Path=Counter}" />
</Grid>
</UserControl>
Code Behind:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
namespace BindingTest
{
public partial class SomeControl : UserControl
{
public SomeControl()
{
InitializeComponent();
var timer = new DispatcherTimer();
timer.Interval = new TimeSpan(0, 0, 5);
timer.Tick += (s, e) => Counter = Counter + 1;
timer.Start();
}
public int Counter
{
get { return (int)GetValue(CounterProperty); }
set { SetValue(CounterProperty, value); }
}
public static readonly DependencyProperty CounterProperty = DependencyProperty.Register(nameof(Counter), typeof(int), typeof(SomeControl), new PropertyMetadata(0));
}
}
So the Control just shows a TextBlock and every 5 seconds increments the Counter. And then I have a consumer of course:
MainWindow XAML:
<Window x:Class="BindingTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BindingTest"
x:Name="MainWindowName" Width="200" Height="300">
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel>
<local:SomeControl Counter="{Binding ElementName=MainWindowName, Path=SomeSource, Mode=OneWay}" />
<local:SomeControl Counter="{Binding ElementName=MainWindowName, Path=SomeSource, Mode=TwoWay}" />
</StackPanel>
</Grid>
</Window>
And lastly the Main Code behind:
using System;
using System.Windows;
using System.ComponentModel;
using System.Windows.Threading;
namespace BindingTest
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
var timer = new DispatcherTimer();
timer.Interval = new TimeSpan(0, 0, 1);
timer.Tick += (s, e) => SomeSource = SomeSource + 1;
timer.Start();
}
private int someSource;
public int SomeSource
{
get => someSource;
set
{
if (someSource != value)
{
someSource = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SomeSource)));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
Right, so the main has a counter in code behind that updates a property every second. The XAML has 2 instances of the UserControl. One that has OneWay binding, and one that has TwoWay binding.
What I see here, is that when the Counter in "SomeControl.cs" updates, the binding for the first UserControl (OneWay) is broken. The one with TwoWay keeps on updating.
Is this by design (and why)? And more importantly, if I have the need for updating properties in my UserControls, how would I do this in my example - in order to support OneWay bindings? Mind that, I'm really not interested in TwoWay binding in this example because it would update "MySource" which is not what I wanted!
Thanks.

It's by design. When you assign a so-called local value to a dependency property, a previously assigned OneWay Binding is replaced. A TwoWay Binding remains active and updates its source property.
There is however a workaround. Do not set a local value, but a "current value". Replace
timer.Tick += (s, e) => Counter = Counter + 1;
with
timer.Tick += (s, e) => SetCurrentValue(CounterProperty, Counter + 1);

Related

Xceed WPF Toolkit - BusyIndicator - creating dynamic loading messages in C#, data binding in a data template

I have a small WPF app that I am working on that uses the Xceed BusyIndicator. I'm having some trouble dynamically updating the loading message because the content is contained within a DataTemplate. The methods I'm familiar with for data binding, or setting the value of text aren't working.
I've done some research - and it looks like others have had this issue. It seems it was answered here, but because the answer was not in context I cannot quite figure out how that would work in my code.
Here is my sample code, if someone could help me understand what I'm missing I would greatly appreciate it. This has the added challenge of using a BackgroundWorker thread. I use this because I anticipate this will be a long running progress - ultimately the action will start a SQL Job that will process items that may take up to 15 minutes. My plan is to have the thread periodically run a stored procedure to get a count of remaining items to process and update the loading message.
MainWindow.xaml:
<Window x:Class="WPFTest.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:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:local="clr-namespace:WPFTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<xctk:BusyIndicator x:Name="AutomationIndicator">
<xctk:BusyIndicator.BusyContentTemplate>
<DataTemplate>
<StackPanel Margin="4">
<TextBlock Text="Sending Invoices" FontWeight="Bold" HorizontalAlignment="Center"/>
<WrapPanel>
<TextBlock Text="Items remaining: "/>
<TextBlock x:Name="_ItemsRemaining" Text="{Binding Path=DataContext.ItemsRemaining, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"/>
</WrapPanel>
</StackPanel>
</DataTemplate>
</xctk:BusyIndicator.BusyContentTemplate>
<Grid>
<StackPanel>
<TextBlock Text="Let's test this thing" />
<Button x:Name="_testBtn" Content="Start" Width="100" HorizontalAlignment="Left" Click="testBtn_Click"/>
<TextBlock Text="{Binding ItemsRemaining}"/>
</StackPanel>
</Grid>
</xctk:BusyIndicator>
</Window>
MainWindow.xaml.cs:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WPFTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
UpdateItemsRemaining(0);
}
public class ItemCountDown
{
//One Idea was to try and set a data binding variable
public string ItemsRemaining { get; set; }
}
public void UpdateItemsRemaining(int n)
{
ItemCountDown s = new ItemCountDown();
{
s.ItemsRemaining = n.ToString();
};
//this.AutomationIndicator.DataContext = s; Works during initiation, but not in the thread worker.
}
private void testBtn_Click(object sender, RoutedEventArgs e)
{
//Someone clicked the button, run the Test Status
TestStatus();
}
public void TestStatus()
{
// Normally I'd start a background worker to run a loop to check that status in SQL
BackgroundWorker getStatus = new BackgroundWorker();
getStatus.DoWork += (o, ea) =>
{
//Normally there's a sql connection being opened to check a SQL Job, and then I run a loop that opens the connection to check
//the status until it either fails or successfully ended.
//but for this test, I'll just have it run for 15 seconds, counting down fake items.
int fakeItems = 8;
do //
{
//Idea One - write to the text parameter. But can't find it in the template
//Dispatcher.Invoke((Action)(() => _ItemsRemaining.Text = fakeItems));
//Idea two - use data binding to update the value. Data binding works just find outside of the Data Template but is ignored in the template
UpdateItemsRemaining(fakeItems);
//subtract one from fake items and wait a second.
fakeItems--;
Thread.Sleep(1000);
} while (fakeItems > 0);
};
getStatus.RunWorkerCompleted += (o, ea) =>
{
//work done, end it.
AutomationIndicator.IsBusy = false;
};
AutomationIndicator.IsBusy = true;
getStatus.RunWorkerAsync();
}
}
}
Thank you for reviewing and I appreciate any help or direction given.
Set the DataContext to your ItemCountDown object and implement INotifyPropertyChanged:
public class ItemCountDown : INotifyPropertyChanged
{
private string _itemsRemaining;
public string ItemsRemaining
{
get { return _itemsRemaining; }
set { _itemsRemaining = value; NotifyPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public partial class MainWindow : Window
{
private readonly ItemCountDown s = new ItemCountDown();
public MainWindow()
{
InitializeComponent();
DataContext = s;
UpdateItemsRemaining(0);
}
public void UpdateItemsRemaining(int n)
{
s.ItemsRemaining = n.ToString();
}
private void testBtn_Click(object sender, RoutedEventArgs e)
{
TestStatus();
}
public void TestStatus()
{
...
}
}
You can then bind directly to the property in the DataTemplate of the XAML markup:
<TextBlock x:Name="_ItemsRemaining" Text="{Binding Path=DataContext.ItemsRemaining, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"/>

"Scrolling"-only Binding update of ItemsSource of ItemsControl. How to make refresh automatic? [duplicate]

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!

Displaying random numbers with a DispatcherTimer

I've been looking for an answer but none seem to fit my question.
I am trying to adapt the MvVM method, but I dont think I fully understand it..
I'm trying to create an RPM display in WPF.
I want it to display an number (between 0-3000) and update this number every second (into a TextBlock).
I have created a new class where I try to create a DispatcherTimer and Random generator and then put that in the UI TextBlock.
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Threading;
namespace Aldeba.UI.WpfClient
{
public class GenerateRpm
{
public GenerateRpm()
{
DispatcherTimer timer = new DispatcherTimer
{
Interval = new TimeSpan(0, 0, 5)
};
timer.Start();
timer.Tick += Timer_Tick;
}
public int RandomValue()
{
Random random = new Random();
int RandomRpm = random.Next(0, 3001);
return RandomRpm;
}
void Timer_Tick(object sender, EventArgs e)
{
MainWindow mainWindow = new MainWindow();
GenerateRpm rpm = new GenerateRpm();
mainWindow.RpmDisplayLabel.Text = rpm.RandomValue().ToString();
}
}
}
My MainWindow.xaml.cs looks like...
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
this.DataContext = new GenerateRpm();
}
}
}
Do I need to add datacontext to all classes I want to access (for bindings for an example)?
This is the MainWindow where I want the Rpm displayed in the second TextBlock.
<StackPanel Orientation="Horizontal" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center">
<TextBlock Text="RPM:" Style="{StaticResource RpmDisplay}" />
<TextBlock x:Name="RpmDisplayLabel" Text="{Binding }" Style="{StaticResource RpmDisplay}" />
</StackPanel>
What am I missing and/ or doing wrong to be able to do this?
Use a view model like shown below, with a public property that is cyclically updated by the timer.
Make sure the property setter fires a change notification, e.g. the PropertyChanged event of the INotifyPropertyChanged interface.
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private readonly Random random = new Random();
public MainWindowViewModel()
{
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
timer.Tick += Timer_Tick;
timer.Start();
}
private int randomRpm;
public int RandomRpm
{
get { return randomRpm; }
set
{
randomRpm = value;
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs(nameof(RandomRpm)));
}
}
private void Timer_Tick(object sender, EventArgs e)
{
RandomRpm = random.Next(0, 3001);
}
}
Assign an instance of the view model class to the MainWindow's DataContext:
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
In the view, bind an element to the view model property:
<TextBlock Text="{Binding RandomRpm}"/>

How can updating a Canvas attached property also update a bound view model property?

I'm changing the position of a UIElement within a WPF Canvas by using the static Canvas.SetTop method in the code-behind (in the full application I'm using a complex Rx chain but for this example I've simplified it to a button click).
The problem I have is that the value of the attached property, Canvas.Top in the XAML, is bound to a property in my ViewModel. Calling Canvas.SetTop bypasses the set in my ViewModel so I don't get the updated value. How can I update the Canvas.Top value in the code-behind so that the ViewModel properties' setter is called?
XAML View:
<Window x:Class="WpfApplication1.MainWindowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="300" Width="300">
<Grid>
<Canvas>
<Button Content="Move Button" Canvas.Top="{Binding ButtonTop}" Click="ButtonBase_OnClick" />
</Canvas>
</Grid>
</Window>
Code-behind:
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication1
{
public partial class MainWindowView : Window
{
public MainWindowView()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
Canvas.SetTop((UIElement) sender, Canvas.GetTop((UIElement) sender) + 5);
}
}
}
ViewModel:
using System.Windows;
namespace WpfApplication1
{
public class MainWindowViewModel : DependencyObject
{
public static readonly DependencyProperty ButtonTopProperty = DependencyProperty.
Register("ButtonTop", typeof(int), typeof(MainWindowViewModel));
public int ButtonTop
{
get { return (int) GetValue(ButtonTopProperty); }
set { SetValue(ButtonTopProperty, value); }
}
public MainWindowViewModel()
{
ButtonTop = 15;
}
}
}
First of all you need to set Binding Mode to TwoWay:
<Button Content="Move Button" Canvas.Top="{Binding ButtonTop, Mode=TwoWay}"
Click="ButtonBase_OnClick" />
Also, if you are setting it from code behind, set using SetCurrentValue() method otherwise binding will be broken and ViewModel instance won't be updated:
UIElement uiElement = (UIElement)sender;
uiElement.SetCurrentValue(Canvas.TopProperty, Canvas.GetTop(uiElement) + 5);
Like mentioned here, do not write code in wrapper properties of DP's:
The WPF binding engine calls GetValue and SetValue directly (bypassing
the property setters and getters).
If you need to synchronize on property change, create a PropertyChangedCallback and do synchronization over there:
public static readonly DependencyProperty ButtonTopProperty = DependencyProperty.
Register("ButtonTop", typeof(int), typeof(MainWindowViewModel),
new UIPropertyMetadata(ButtonTopPropertyChanged));
private static void ButtonTopPropertyChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs args)
{
// Write synchronization logic here
}
Otherwise simply have normal CLR property and you should consider implementing INotifyPropertyChanged on your class:
private double buttonTop;
public double ButtonTop
{
get { return buttonTop; }
set
{
if(buttonTop != value)
{
// Synchronize here
buttonTop = value;
}
}
}

How To Prevent WPF DataGrid From De-Selecting SelectedItem When Items Updated?

My scenario: I have a background thread that polls for changes and periodically updates a WPF DataGrid's ObservableCollection (MVVM-style). The user can click on a row in the DataGrid and bring up the "details" of that row in an adjacent UserControl on the same main view.
When the background thread has updates, it cycles through the objects in the ObservableCollection and replaces individual objects if they have changed (in other words, I am not rebinding a whole new ObservableCollection to the DataGrid, but instead replacing individual items in the collection; this allows the DataGrid to maintain sorting order during updates).
The problem is that after a user has selected a specific row and the details are displayed in the adjacent UserControl, when the background thread updates the DataGrid the DataGrid loses the SelectedItem (it gets reset back to index of -1).
How can I retain the SelectedItem between updates to the ObservableCollection?
If your grid is single-selection, my suggestion is that you use the CollectionView as the ItemsSource instead of the actual ObservableCollection. Then, make sure that Datagrid.IsSynchronizedWithCurrentItem is set to true. Finally, at the end of your "replace item logic", just move the CollectionView's CurrentItem to the corresponding new item.
Below is a sample that demonstrates this. (I'm using a ListBox here though. Hope it works fine with your Datagrid).
EDIT - NEW SAMPLE USING MVVM:
XAML
<Window x:Class="ContextTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="window"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<ListBox x:Name="lb" DockPanel.Dock="Left" Width="200"
ItemsSource="{Binding ModelCollectionView}"
SelectionMode="Single" IsSynchronizedWithCurrentItem="True">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Text="{Binding ElementName=lb, Path=SelectedItem.Description}"/>
</DockPanel>
</Window>
Code-Behind:
using System;
using System.Windows;
using System.Windows.Data;
using System.Collections.ObjectModel;
using System.Windows.Threading;
namespace ContextTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
public class ViewModel
{
private DataGenerator dataGenerator;
private ObservableCollection<Model> modelCollection;
public ListCollectionView ModelCollectionView { get; private set; }
public ViewModel()
{
modelCollection = new ObservableCollection<Model>();
ModelCollectionView = new ListCollectionView(modelCollection);
//Create models
for (int i = 0; i < 20; i++)
modelCollection.Add(new Model() { Name = "Model" + i.ToString(),
Description = "Description for Model" + i.ToString() });
this.dataGenerator = new DataGenerator(this);
}
public void Replace(Model oldModel, Model newModel)
{
int curIndex = ModelCollectionView.CurrentPosition;
int n = modelCollection.IndexOf(oldModel);
this.modelCollection[n] = newModel;
ModelCollectionView.MoveCurrentToPosition(curIndex);
}
}
public class Model
{
public string Name { get; set; }
public string Description { get; set; }
}
public class DataGenerator
{
private ViewModel vm;
private DispatcherTimer timer;
int ctr = 0;
public DataGenerator(ViewModel vm)
{
this.vm = vm;
timer = new DispatcherTimer(TimeSpan.FromSeconds(5),
DispatcherPriority.Normal, OnTimerTick, Dispatcher.CurrentDispatcher);
}
public void OnTimerTick(object sender, EventArgs e)
{
Random r = new Random();
//Update several Model items in the ViewModel
int times = r.Next(vm.ModelCollectionView.Count - 1);
for (int i = 0; i < times; i++)
{
Model newModel = new Model()
{
Name = "NewModel" + ctr.ToString(),
Description = "Description for NewModel" + ctr.ToString()
};
ctr++;
//Replace a random item in VM with a new one.
int n = r.Next(times);
vm.Replace(vm.ModelCollectionView.GetItemAt(n) as Model, newModel);
}
}
}
}
OLD SAMPLE:
XAML:
<Window x:Class="ContextTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<ListBox x:Name="lb" SelectionMode="Single" IsSynchronizedWithCurrentItem="True" SelectionMode="Multiple">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Text="{Binding ElementName=lb, Path=SelectedItem.Name}"/>
<Button Click="Button_Click">Replace</Button>
</StackPanel>
</Window>
Code-behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace ContextTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
ObservableCollection<MyClass> items;
ListCollectionView lcv;
public MainWindow()
{
InitializeComponent();
items = new ObservableCollection<MyClass>();
lcv = (ListCollectionView)CollectionViewSource.GetDefaultView(items);
this.lb.ItemsSource = lcv;
items.Add(new MyClass() { Name = "A" });
items.Add(new MyClass() { Name = "B" });
items.Add(new MyClass() { Name = "C" });
items.Add(new MyClass() { Name = "D" });
items.Add(new MyClass() { Name = "E" });
}
public class MyClass
{
public string Name { get; set; }
}
int ctr = 0;
private void Button_Click(object sender, RoutedEventArgs e)
{
MyClass selectedItem = this.lb.SelectedItem as MyClass;
int index = this.items.IndexOf(selectedItem);
this.items[index] = new MyClass() { Name = "NewItem" + ctr++.ToString() };
lcv.MoveCurrentToPosition(index);
}
}
}
I haven't worked with the WPF DataGrid, but I'd try this approach:
Add a property to the view-model that will hold the value of the currently selected item.
Bind SelectedItem to this new property using TwoWay.
This way, when the user selects a row, it will update the view-model, and when the ObservableCollection gets updated it won't affect the property to which SelectedItem is bound. Being bound, I wouldn't expect it could reset in the way you're seeing.
You could, in the logic that updates the Collection, save off the CollectionView.Current item reference to another variable. Then, after you're done updating, call CollectionView.MoveCurrentTo(variable) to reset the selected item.
Its probably resolved by now, but here is an example of what I did and it works for a grid of carts.
I have a datagrid with ObservableCollection and CollectionView, populated from local variable containing carts:
_cartsObservable = new ObservableCollection<FormOrderCart>(_formCarts);
_cartsViewSource = new CollectionViewSource { Source = _cartsObservable };
CartsGrid.ItemsSource = _cartsViewSource.View;
Later I change Valid prop of carts in a function - not directly, but important is that there is a change in item in ObservableCollection. To reflect the change and maintain selection I just refresh the CollectionViewSource (notice the inside View):
var cart = _formCarts.ElementAt(index-1);
cart.Valid = validity;
_cartsViewSource.View.Refresh();
This way I am able to change the row color in grid to red if the cart is invalid, but also keep my selection.
EDIT: Spelling

Categories