WPF UserControl command binding not udpateing UI MVVM - c#

I have window "ClientsWindow" and it's view model class "ClientsViewModel". In ViewModel i defined property "Clients" and bound it to DataGrid's itemssource property:
private ObservableCollection<tblClient> clients;
public ObservableCollection<tblClient> Clients
{
get { return clients; }
set
{
clients = value;
OnPropertyChanged("Clients");
}
}
In my window's constructor I set this property to new value by calling the method from wcf service like this:
Clients = new ObservableCollection<tblClient>(wcf.FilterClients(PageIndex, PageSize));
And it works perfect, I get 10 records from wcf service as it should be and the list is shown in datagrid. I insert some usercontrol which I want to use for datagrid pagination. It has ChangedIndexCommand defined like this:
ChangedIndexCommandProperty =
DependencyProperty.Register("ChangedIndexCommand", typeof(ICommand), typeof(GridPaging), new UIPropertyMetadata(null));
public ICommand ChangedIndexCommand
{
get { return (ICommand)GetValue(ChangedIndexCommandProperty); }
set { SetValue(ChangedIndexCommandProperty, value); }
}
I tried to bind command form my window's viewmodel to this command, so i did it this way:
private ICommand _cmdChangedIndex;
public ICommand cmdChangedIndex
{
get
{
if (_cmdChangedIndex == null)
{
_cmdChangedIndex = new DelegateCommand(delegate()
{
worker.DoWork += worker_FilterClientsList;
worker.RunWorkerCompleted += worker_FilterClientListCompleted;
worker.RunWorkerAsync();
});
}
return _cmdChangedIndex;
}
}
private void worker_FilterClientsList(object sender, DoWorkEventArgs e)
{
try
{
ServiceClient wcf = new ServiceClient();
Clients = new ObservableCollection<tblClient>(wcf.FilterClients(PageIndex, PageSize));
TotalCount = wcf.ReturnClientsCount();
}
catch (Exception ex)
{
}
}
private void worker_FilterClientListCompleted(object sender, RunWorkerCompletedEventArgs e)
{
worker.DoWork -= worker_FilterClientsList;
}
And here is the xaml:
<pc:GridPaging PageIndex="{Binding PageIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PageSize="{Binding PageSize, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TotalCount="{Binding TotalCount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" x:Name="clientsPagingControl"
ChangedIndexCommand="{Binding cmdChangedIndex, UpdateSourceTrigger=PropertyChanged}"
Visibility="Visible" VerticalAlignment="Top"
/>
So, while debugging everything works perfect! My command is fired when i click on the button of my userconrol, the method from wcf service is called properly and it returns new collection of items(count 2, as expected), my "Clients" property is set to new value BUT, UI still showing 10 items in my datagrid. I just cant figure out what is wrong?! Is this wrong way of binding commands to custom user controls?? Also let me note that, PageIndex, PageSize and TotalCount properties are of type int, and i bound them to my viewmodel properties, and they work perfect. But what is the problem with my command? I tried to be as clear as I could hope that you will understand what my problem is, and for any more info, please leave the comment.
OnPropertyChanged:
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
DataGrid binding:
<DataGrid IsReadOnly="True" Name="dgClients" AutoGenerateColumns="False" ItemsSource="{Binding Path=Clients, UpdateSourceTrigger=PropertyChanged}">
<DataGrid.Columns>
....
</DataGrid.Columns>
</DataGrid>

Just a thought, but it looks like you are using a BackgroundWorker class in your ICommand? In the worker_FilterClientsList method, you are setting the "Clients" observable collection property. I don't think you are able to manipulate the UI from within DoWork (it's running on a different thread). Try removing the try..catch block to see if it's hiding such an error.
You normally have to update the UI from the RunWorkerCompleted delegate (your worker_FilterClientListCompleted method).

Ok, so judging by your question, answers and the many comments, it would seem that your problem is un-reproducible. This means that you are on your own, as far as fixing your problem goes. However, this is not as bad as it sounds.
As there is no obvious problem with your displayed code, I cannot point out where your error lies. However, I can put you onto the right path to fix your own problem. It will take some time and effort on your part, but 'no pain... no gain', as they say.
One of the best ways that you find the problem in a complex project is to simplify it in a new, empty project. Normally when doing this, one of two things happens: either you find out what the problem was, or you create a concise working example that demonstrates your problem, which you can then post here (maybe as a new question, or instead of your current code). It's usually a win-win situation.
As it happens, the StackOverflow Help Center has a page to help with this. Please follow the advice in the How to create a Minimal, Complete, Tested and Readable example page to help you to simplify your problem.
One final point that I'd like to make is that normally in an application, the data access layer is separate from the UI. If you separate your different concerns like this, you will also find that it simplifies the situation further.

Related

ReactiveUI vs. ICollectionView

I have a .Net 4.5 app that is moving to WPF-based RxUI (kept up to date, 6.0.3 as of this writing). I have a text field that should function as a filter field with the fairly common throttle etc. stuff that was part of the reason for going reactive in the first place.
Here is the relevant part of my class.
public class PacketListViewModel : ReactiveObject
{
private readonly ReactiveList<PacketViewModel> _packets;
private PacketViewModel _selectedPacket;
private readonly ICollectionView _packetView;
private string _filterText;
/// <summary>
/// Gets the collection of packets represented by this object
/// </summary>
public ICollectionView Packets
{
get
{
if (_packets.Count == 0)
RebuildPacketCollection();
return _packetView;
}
}
public string FilterText
{
get { return _filterText; }
set { this.RaiseAndSetIfChanged(ref _filterText, value); }
}
public PacketViewModel SelectedPacket
{
get { return _selectedPacket; }
set { this.RaiseAndSetIfChanged(ref _selectedPacket, value); }
}
public PacketListViewModel(IEnumerable<FileViewModel> files)
{
_packets = new ReactiveList<PacketViewModel>();
_packetView = CollectionViewSource.GetDefaultView(_packets);
_packetView.Filter = PacketFilter;
_filterText = String.Empty;
this.WhenAnyValue(x => x.FilterText)
.Throttle(TimeSpan.FromMilliseconds(300)/*, RxApp.TaskpoolScheduler*/)
.DistinctUntilChanged()
.ObserveOnDispatcher()
.Subscribe(_ => _packetView.Refresh());
}
private bool PacketFilter(object item)
{
// Filter logic
}
private void RebuildPacketCollection()
{
// Rebuild packet list from data source
_packetView.Refresh();
}
}
I unit test this using Xunit.net with Resharper's test runner. I create some test data and run this test:
[Fact]
public void FilterText_WhenThrottleTimeoutHasPassed_FiltersProperly()
{
new TestScheduler().With(s =>
{
// Arrange
var fvm = GetLoadedFileViewModel();
var sut = new PacketListViewModel(fvm);
var lazy = sut.Packets;
// Act
sut.FilterText = "Call";
s.AdvanceToMs(301);
// Assert
var res = sut.Packets.OfType<PacketViewModel>().ToList();
sut.Packets.OfType<PacketViewModel>()
.Count().Should().Be(1, "only a single packet should match the filter");
});
}
I put a debug statement on the Subscribe action for my FilterText config in the constructor of the class, and it gets called once for each packet item at startup, but it never gets called after I change the FilterText property.
Btw, the constructor for the test class contains the following statement to make threading magic work:
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
My problem is basically that the Refresh() method on my view never gets called after I change the FilterText, and I can't see why not.
Is this a simple problem with my code? Or is this a problem with a CollectionViewSource thing running in a unit testing context rather than in a WPF context?
Should I abandon this idea and rather have a ReactiveList property that I filter manually whenever a text change is triggered?
Note: This works in the application - the FilterText triggers the update there. It just doesn't happen in the unit test, which makes me wonder whether I am doing it wrong.
EDIT: As requested, here are the relevant bits of XAML - this is for now just a simple window with a textbox and a datagrid.
The TextBox:
<TextBox Name="FilterTextBox"
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding FilterText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
/>
The datagrid:
<DataGrid ItemsSource="{Binding Path=Packets}"
Name="PacketDataGrid"
SelectedItem="{Binding SelectedPacket}"
AutoGenerateColumns="False"
EnableRowVirtualization="True"
SelectionMode="Single"
SelectionUnit="FullRow"
CanUserAddRows="False"
CanUserResizeRows="False"
>
<DataGrid.Columns>
...
If anything else is relevant/needed, let me know!
EDIT 2: Paul Betts recommends not doing the SynchronizationContext setup in the test constructor like I do, probably for very valid reasons. However, I do this because of the way another viewmodel (FileViewModel) works - it needs to wait for a MessageBus message to know that packet processing is complete. This is something that I am working actively on trying to avoid - I know the MessageBus is a very convenient bad idea. :) But this is the cause for the SyncContext stuff. The method that creates the test viewmodel looks like this:
private FileViewModel GetLoadedFileViewModel()
{
var mre = new ManualResetEventSlim();
var fvm = new FileViewModel(new MockDataLoader());
MessageBus.Current
.Listen<FileUpdatedPacketListMessage>(fvm.MessageToken.ToString())
.Subscribe(msg => mre.Set());
fvm.LoadFile("irrelevant.log");
mre.Wait(500);
return fvm;
}
I realize this is bad design, so please don't yell. ;) But I am taking a lot of legacy code here and moving it into RxUI based MVVM - I can't do it all and end up with a perfect design just yet, which is why I am getting unit tests in place for all this stuff so that I can do Rambo refactoring later. :)
Btw, the constructor for the test class contains the following statement to make threading magic work:
Don't do this
My problem is basically that the Refresh() method on my view never gets called after I change the FilterText, and I can't see why not.
I believe your problem is the commented out part:
.Throttle(TimeSpan.FromMilliseconds(300)/, RxApp.TaskpoolScheduler/)
And this part:
.ObserveOnDispatcher()
When you use TestScheduler, you must use RxApp.[MainThread/Taskpool]Scheduler for all scheduler parameters. Here above, you're using a real TaskpoolScheduler and a real Dispatcher. Since they're not under TestScheduler, they can't be controlled by TestScheduler.
Instead, write:
this.WhenAnyValue(x => x.FilterText)
.Throttle(TimeSpan.FromMilliseconds(300), RxApp.TaskpoolScheduler)
.DistinctUntilChanged()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(_ => _packetView.Refresh());
and everything should work.

Cannot bind to a Property of DataContext

This is my first time posting a question. I'm looking into this issue for about a whole day but cannot see why this binding doesn't work.
I want a Label to display the name of a object "hotspot" which is a Property of class instance named Plan. There are multiple plans and each plan contains multiple hotspots. When I click on a hotspot the property Plan.SelectedHotSpot sets this clicked hotspot as value. If there is no HotSpot selected it turns to null.
XAML:
<Label Name="lblHotSpotName" />
MainWindow code behind when Plan is selected from ListBox:
private void lstPlans_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
canvas.Plan = PlanBLL.GetPlanByID(plans[lstPlans.SelectedIndex].ID);
lblHotSpotName.DataContext = canvas.Plan;
lblHotSpotName.SetBinding(Label.ContentProperty, "SelectedHotSpot.Name");
}
Plan class:
public class Plan : INotifyPropertyChanged
{
private HotSpot selectedHotSpot;
public HotSpot SelectedHotSpot
{
get { return selectedHotSpot; }
set
{
selectedHotSpot = value;
OnPropertyChanged("SelectedHotSpot");
OnPropertyChanged("SelectedHotSpot.Name");
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
This code doesn't seem to work when I click on a hotspot lblHotSpotName stays empty.
It seems to me that when a plan is loaded SelectedHotSpot is null and so it doesn't bind to that hotspot object which is selected after the plan has been loaded.
Is my insinuation right? That this binding needs to have an existing object which is not null. And when the object changes that we need to define the binding from label to Plan.SelectedHotSpot again.
Thanks for your help.
I can't be sure that I have understood your problem exactly right because your question is somewhat unclear, but can you not just data bind to the Label.Content property in XAML? If you want to data bind the SelectedHotSpot.Name property of the Plan item that is currently selected in the ListBox, then you should be able to do something like this:
<Label Name="lblHotSpotName"
Content="{Binding SelectedItem.SelectedHotSpot.Name, ElementName=lstPlans}" />
UPDATE >>>
You're still better off using XAML for your Binding. Add a string property to bind to and then update that in your lstPlans_SelectionChanged handler instead:
<Label Name="lblHotSpotName" Content="{Binding SelectedItemHotSpotName}" />
...
private void lstPlans_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
canvas.Plan = PlanBLL.GetPlanByID(plans[lstPlans.SelectedIndex].ID);
SelectedItemHotSpotName = canvas.Plan.SelectedHotSpot.Name;
}
I'm not sure it will help or not, but in lstPlans_SelectionChanged try this Binding:
var myBinding = new Binding();
myBinding.Path = new PropertyPath("SelectedHotSpot.Name");
myBinding.Source = canvas.Plan;
lblHotSpotName.SetBinding(Label.ContentProperty, myBinding);
If SelectedHotSpot.Name doesn't change, when this line is not needed:
OnPropertyChanged("SelectedHotSpot.Name");
in SelectedHotSpot property declaration.
Don't see any issue in the given code (though, Raise property changed for .Name is not required).
I would suggest to confirm that selectedHotSpot always has some instance and is not null.
Try modifying your plan class and set:
selectedHotSpot = new HotSpot(Name="Default")
and you should see "Default" in your label.

Operation is not valid while ItemsSource is in use. Access and modify elements with ItemsControl.ItemsSource instead when execution twice

I have a piece of code doesn't work properly. If I execute the btnNew once there is no problem. If I execute twice I get a error of...
Operation is not valid while ItemsSource is in use. Access and modify elements with ItemsControl.ItemsSource instead.
Main class
ClassA obj = new ClassA();
private void btnNew_Click(object sender, RoutedEventArgs e)
{
//List strings for clearing and then creating new strings for Title combobox
ObservableCollection<string> calledList = obj.GetList();
cbTitle.Items.Clear();
cbTitle.ItemsSource = calledList;
}
ClassA.cs
private ObservableCollection<string> data = new ObservableCollection<string>();
public ObservableCollection<string> GetList()
{
return data;
}
public void SimpleNew()
{
data.Add("A");
data.Add("B");
}
if I use a if statement in the main class it will eliminate the problem then it will create duplicate strings in the combobox. Then I am asking myself do I need to create a method to handle distinct? I am not sure on this.
This my if statement in the main class
if (cbTitle.Items.Count == 0)
{
ObservableCollection<string> calledList = obj.GetList();
cbTitle.Items.Clear();
cbTitle.ItemsSource = calledList;
}
When I used try/catch it catches error and shows the message. So this is not good either.
So my question is can anyone tell me how to solve this problem?
You cannot set both the ItemsSource property and the Items property together. Try simply removing your calls to cbTitle.Items.Clear() which is unnecessary if you are setting the ItemsSource property on the next line anyway.
UPDATE >>>
You only need to set the ItemsSource property once, preferably in XAML:
<ComboBox ItemsSource="{Binding Items}" ... />
Once this is done, you shouldn't set it again. To change the items in the ComboBox, simply change the items in the collection... they are now data bound... this is WPF, not WinForms:
private void btnNew_Click(object sender, RoutedEventArgs e)
{
//List strings for clearing and then creating new strings for Title combobox
ObservableCollection<string> calledList = obj.GetList();
Items = calledList;
}
Thanks Sheridan. I have also discovered that if I do...
ObservableCollection<string> calledList = obj.GetList();
calledList.Clear(); // I have to use this line of code
calledList.ItemsSource = calledList;
This solve my problem. I am not using xaml because it gave me problems. You may remember I opened a thread about combobox when navigating through records. I managed to solve that problem by using for loop. have a look at my other thread, if you wish, here
However this is not the final solution. I am learning wpf and its cud operation so it will be interesting what I will discover

Multiple Views of Observable Collection with Datagrid

I have the same problem like this. But I´m using a DataGrid instead of a ListBox and it does not seem to work like this (it might also be because i never used visual basic and didnt translate the code correcly into c#).
I basicly want two DataGrids on the same data with different filters.
ICollectionView view_dataLinesUnfiltered;
ICollectionView view_dataLinesFiltered;
public MainWindow()
{
...
//view_dataLines = CollectionViewSource.GetDefaultView(dataLines); // <- Filter works on both
view_dataLinesUnfiltered = new CollectionView(dataLines); // <- Filter doesn´t work at all
view_dataLinesFiltered = new CollectionView(dataLines);
....
// Control Events
this.ShowAA.RaiseEvent(new RoutedEventArgs(System.Windows.Controls.Primitives.ToggleButton.UncheckedEvent));
}
private void ShowAA_Checked(object sender, RoutedEventArgs e)
{
view_dataLinesUnfiltered.Filter = null;
}
private void ShowAA_UnChecked(object sender, RoutedEventArgs e)
{
view_dataLinesUnfiltered.Filter = delegate(object o) { return FilterContent(o as ErrorDetection.stDataLine, "AA", ""); };
}
bool FilterContent(ErrorDetection.stDataLine line, string sFilterAA, string sFilter)
{
shortArrayToHexStringConverter converter = new shortArrayToHexStringConverter();
string comBuffer = converter.Convert(line.ComBufferP as object,typeof(string),0,System.Globalization.CultureInfo.CurrentCulture) as string;
return false;// !comBuffer.Contains("AA");
}
The FilterContent method is being called without problems, but the DataGrid shows the lines anyway. If I use GetDefaultView the Filter works on both Datagrids. Do I have to use some other view instead of CollectionView (ListCollectionView does also not work)?
i have made a small sample project to show the problem sample. It only consists of an constructor and an observable collection.
I got it to work somehow. I used CollectionViewSources now instead of ICollectionView.
<Window.Resources>
<CollectionViewSource x:Key="viewSource_dataLinesUnfiltered"/>
<CollectionViewSource x:Key="viewSource_dataLinesFiltered"/>
</Window.Resources>
...
<DataGrid Name="Filtered_Datagrid" ItemsSource="{Binding Source={StaticResource viewSource_dataLinesFiltered}}" >
...
</DataGrid>
...
<DataGrid Name="Unfiltered_Datagrid" ItemsSource="{Binding Source={StaticResource viewSource_dataLinesUnfiltered}}">
...
</DataGrid>
and the c Code:
CollectionViewSource viewSource_dataLinesUnfiltered;
CollectionViewSource viewSource_dataLinesFiltered;
...
public MainWindow()
{
InitializeComponent();
this.DataContext = dataLines;
viewSource_dataLinesUnfiltered = (CollectionViewSource)this.Resources["viewSource_dataLinesUnfiltered"];
viewSource_dataLinesUnfiltered.Source = dataLines;
viewSource_dataLinesFiltered = (CollectionViewSource)this.Resources["viewSource_dataLinesFiltered"];
viewSource_dataLinesFiltered.Source = dataLines;
// Control Events
this.ShowAA.RaiseEvent(new RoutedEventArgs(System.Windows.Controls.Primitives.ToggleButton.UncheckedEvent));
}
private void ShowAA_Checked(object sender, RoutedEventArgs e)
{
viewSource_dataLinesUnfiltered.View.Filter = null;
}
private void ShowAA_UnChecked(object sender, RoutedEventArgs e)
{
viewSource_dataLinesUnfiltered.View.Filter = delegate(object o) { return FilterContent(o as ErrorDetection.stDataLine, "AA", ""); };
}
bool FilterContent(ErrorDetection.stDataLine line, string sFilterAA, string sFilter)
{
shortArrayToHexStringConverter converter = new shortArrayToHexStringConverter();
string comBuffer = converter.Convert(line.ComBufferP as object,typeof(string),0,System.Globalization.CultureInfo.CurrentCulture) as string;
return !comBuffer.Contains("AA");
}
But I´m not sure why it works this way and the filter is not applied on window repaints when ICollectionView is used.
You need to specify which ICollectionVIew is used on which DataGrid.
If you just bind to the collection (dataLines in this case) WPF will use the 'default view' (or create one if necessary), this is why the first commented out line works for filtering.
There are a few ways you could specify which view is used for which datagrid, depending on what patterns, etc. you are using
1) Like the linked question, you could set the ItemsSource for each DataGrid in the window's code behind, after initializing the views, e.g.:
filteredDataGrid.ItemsSource = view_dataLinesFiltered;
unfilteredDataGrid.ItemsSource = view_dataLinesUnfiltered;
2) You could set the DataContext of the window to itself, or make a view model for the screen that contains the view, and make the views public properties and then bind to the intended view for each grid, e.g.
<DataGrid ItemsSource="{Binding View_dataLinesFiltered}"> ....
Edit:
Now I'm not at work and can get to dropbox and play with your example it seems like the cause of the weird behaviour is the use of CollectionView directly. On the msdn page for CollectionView it says
You should not create objects of this class in your code. To create a
collection view for a collection that only implements IEnumerable,
create a CollectionViewSource object, add your collection to the
Source property, and get the collection view from the View property.
However, if you don't want to set up the views in XAML, you could also change your CollectionViews to ListCollectionViews and it should work as expected (this is likely the view type that CollectionViewSource is making for you behind the scenes anyway).

custom binding .net forms

Is there any way to get custom binding behavior in .net win forms?
Example, I'm connecting my control to a BindingSource object and adding a binding like
this.slider.DataBindings.Add(new System.Windows.Forms.Binding("Enabled", this.bindingSourceModel, "FloatProperty > 0.5f", true));
There's no way the above will work but I like it to be enabled if dataSource.FloatProperty becomes greater than 0.5f.
Is there any way to do this?
I understand what you want to do, so I've slightly modified your situation for the sake of demonstration: the UI setup is obvious, there is a TrackBar and a Button, and the problem here is to bind the Enabled property of button to the boolean value of the expression trackBar.Value > 50.
The idea is to turn the main form into something like a ViewModel (as in MVVM). Observe that I am implementing INotifyPropertyChanged.
public partial class ManiacalBindingForm : Form, INotifyPropertyChanged {
public ManiacalBindingForm() {
InitializeComponent();
this.button.DataBindings.Add("Enabled", this, "ManiacalThreshold", true, DataSourceUpdateMode.OnPropertyChanged);
this.trackBar.ValueChanged += (s, e) => {
this.Text = string.Format("ManiacalBindingForm: {0}", this.trackBar.Value);
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("ManiacalThreshold"));
};
}
public bool ManiacalThreshold {
get { return this.trackBar.Value > 50; }
}
public event PropertyChangedEventHandler PropertyChanged;
...
}
Now, this is my personal observation: While there is a non-trivial interpretation of your goal, your goal is a bit maniacal. You have to ponder why exactly you want to achieve this through data-binding. Binding is mostly aimed for automatic, bi-directional, sync'ing of property values. Doing this type of UI update via binding directly to the "model" is even more maniacal. But you got credit for being maniacal, though! ;-)

Categories