In a Windows Phone 8.1 application (targeting Runtime not Silverlight), I have an ObservableCollection bound to a ListView, defined like this:
<ListView ItemsSource="{Binding ListItems, Mode=TwoWay}" CanReorderItems="True" ReorderMode="Enabled">
<ListView.ItemTemplate>
...etc...
In the ViewModel's constructor, I also have
ListItems.CollectionChanged += ListItems_CollectionChanged;
which is raising the event whenever any items are added and deleted - however, that's all handled from the VM not the View. Unfortunately, the event is not being raised when the items are reorder. I've set this up, moved some items, added a breakpoint in the event handler and added an item, and I can see the underlying ObservableCollection's order has changed as controlled from the View. So why isn't the event raising? And if it won't, what's the best practice for persisting a ListView's order in the database?
UPDATE:
The problem's actually bigger than I thought... it seems that the ListView.CollectionChanged event is not firing when adding an item either! It does when the application starts and loads them from the database but not when added by a user from the UI. This is very strange because the addition of items is performed using the exact same method. From the database:
private ViewModel MapFromModel(Item model, SQLiteAsyncConnection connection)
{
var viewModel = new ViewModel
{
Id = model.Id,
Text = model.Text,
Description = model.Description,
Added = model.Added,
Completed = model.Completed,
DueOn = model.DueOn,
ParentId = model.ParentId,
DisplayOrderNumber = model.DisplayOrderNumber,
IsNew = false
};
foreach (
var childViewModel in
connection.Table<Item>()
.Where(ci => ci.ParentId == viewModel.Id)
.ToListAsync()
.Result.Select(childItem => MapFromModel(childItem, connection)))
{
if (!_cache.Contains(childViewModel))
_cache.Add(childViewModel);
viewModel.AddItem(childViewModel);
}
return viewModel;
}
You see this recursive method calls the ViewModel's AddItem() method to add children (which are of the same type). I also have an ICommand bound to a button to add other items:
public void Execute(object parameter)
{
var viewModel = parameter as ViewModel;
if (viewModel == null) return;
AddItem(viewModel);
}
public static void AddItem(ViewModel viewModel)
{
// The DisplayOrderNumber of the new item needs to be the max of the current collection + 1.
var displayOrderNumber = viewModel.ListItems.Any()
? viewModel.ListItems.Max(ci => ci.DisplayOrderNumber) + 1
: 0;
var newText = string.Format("{0} {1}",
viewModel.Id == Guid.Empty ? "List" : "Item", displayOrderNumber + 1);
var newItem = new ViewModel
{
Text = newText,
NewText = newText,
ParentId = viewModel.Id,
InEditMode = true,
Added = DateTime.Now,
DisplayOrderNumber = displayOrderNumber,
IsNew = true
};
viewModel.AddItem(newItem);
viewModel.Save();
}
So why should the AddItem() method raise the event when called from the Service Layer but not from the ViewModel layer itself?
It turns out, as usual, the fault was entirely mine.
This was occurring because the event handling method ListItems_CollectionChanged was being disconnected. The cause of this was because of a Sort method that was replacing the underlying connection. I've got around this problem by adding the event handler, if needed, in the setter for the property (and of course removing any unused event handlers).
Related
The third or so page in my app contains a ListView, but the list for some reason doesn't display until I either toggle the view (which switches the views ItemSource to another list) or rotate the screen.
If I do the toggle twice (so back to the original starting state) the listview is there still. It seems like a bug but I haven't been able to find anything on it.
public partial class ReviewRequestsPage : ContentPage
{
private readonly List<RequestCell> closedRequestCells = new List<RequestCell>();
private readonly List<RequestCell> openRequestCells = new List<RequestCell>();
public ReviewRequestsPage()
{
InitializeComponent();
NavigationPage.SetHasNavigationBar(this, false);
BindingContext = new SvgImagesViewModels();
new Footer().SetGestureRecognizers(null, Notifications, Help, Home, this);
LoadRequestLists();
ToggleSwitch.PropertyChanged += (o, ea) => { HandleToggle(((Switch) o).IsToggled); };
}
....
private void LoadRequestLists()
{
UserDialogs.Instance.ShowLoading("Loading Requests...", MaskType.Black);
var client = new RestService().Client;
var request =
new RequestService().GetAllRequests();
client.ExecuteAsync(request, response =>
{
var myList = JsonConvert.DeserializeObject<List<Request>>(response.Content, new DateTimeConverter());
myList.ForEach(r =>
{
if (r.status.type == StatusType.CLOSED) closedRequestCells.Add(new RequestCell(r));
else if (r.status.type != StatusType.DELETED) openRequestCells.Add(new RequestCell(r));
});
UserDialogs.Instance.HideLoading();
RequestsList.ItemSource = openRequestCells;
});
}
private void HandleToggle(bool isToggled)
{
Switchlabel.Text = isToggled ? Constants.Closed : Constants.Open;
RequestsList.ItemsSource = isToggled ? closedRequestCells : openRequestCells;
}
Is there something else I should be calling or doing so that the listview appears once I set the ItemSource? It doesn't make sense why it wouldn't be already though. Also nothing is failing and everything is working as expected, other than that
The constructor does not set ItemsSource, at least not immediately. It calls LoadRequestLists that starts an async Task which will eventually set ItemsSource, so at some point in the future, ItemsSource will be set (whenever the Rest response is received and the UI thread happens to run).
Since constructors cannot await an async Task, you will need to refactor your code so that the Rest client runs (and finishes) before the constructor, and so the ReviewRequestsPage will take in the List as a parameter. Then the constructor can build the openRequestCells and closedRequestCells and assign to ItemsSource.
I have a ViewModel which contains an ObservableCollection<CustomKeyGroup<CustomItem>> property bound to a control in a View and the problem is that I want to sort this collection by a property in CustomKeyGroup<T>, without setting the ObservableCollection<...> object property (i.e. sort the collection inline):
public class MainViewModel : ViewModelBase {
... // data service etc code
private ObservableCollection<CustomKeyGroup<CustomItem>> _items = new ObservableCollection<CustomKeyGroup<CustomItem>>();
public ObservableCollection<CustomKeyGroup<CustomItem>> Items
{
get
{
return _items;
}
set
{
_items = value;
RaisePropertyChanged("Items");
}
}
public void Sort(string _orderBy = null, bool _descending = true) {
if (string.IsNullOrEmpty(_orderBy) || this.Items.Count == 0) {
return;
}
var test = this.Items.ToList();
// bubble sort
try {
for (int i = test.Count - 1; i >= 0; i--) {
for (int j = 1; j <= i; j++) {
CustomKeyGroup<CustomItem> o1 = test[j - 1];
CustomKeyGroup<CustomItem> o2 = test[j];
bool move = false;
var order = typeof(CustomKeyGroup<CustomItem>).GetProperty(orderBy);
var t = order.GetValue(o1);
var t2 = order.GetValue(o2);
// sort comparisons depending on property
if (_descending) { // ascending
if (t.GetType() == typeof(int)) { // descending and int property
if ((int)t < (int)t2) {
move = true;
}
} else { // descending and string property
if (t.ToString().CompareTo(t2.ToString()) > 0) {
move = true;
}
}
} else { // ascending
if (t.GetType() == typeof(int)) { // ascending and int property
if ((int)t > (int)t2) {
move = true;
}
} else { // ascending and string property
if (t.ToString().CompareTo(t2.ToString()) < 0) {
move = true;
}
}
}
// swap elements
if (move) {
//this.Items.Move(j - 1, j); // "inline"
test[j] = o1;
test[j - 1] = o2;
}
}
}
// set property to raise property changed event
this.Items = new ObservableCollection<CustomKeyGroup<CustomItem>>(test);
} catch (Exception) {
Debug.WriteLine("Sorting error");
}
//RaisePropertyChanged("Items"); // "inline sort" raise property changed to update Data binding
Debug.WriteLine("Sorted complete");
}
... // get data from service, etc.
From the code above, the attempted inline sorts are commented out (as they do not update the control that databinds to it), and the manual setting of Items are left in (works, but if you scroll down the control and sort, it will take you back to the top - undesirable!).
Anyone have any idea how I can update the view/control using an inline sort option? I've also tried manually raising the RaisePropertyChanged event (specified in ObservableObject using the MVVMLight Toolkit) to no avail.
Note: Setting a breakpoint at the end of the try-catch reveals that the ObservableCollection<...> is indeed sorted, but the changes just do not reflect in the View! Even weirder is that the control (LongListSelector) has a JumpList bound to another property of CustomKeyGroup<T> and it successfully updates instantly!! If I tap on any of these items in the JumpList, the View correctly updates itself, revealing the sorted items... I then thought of setting the DataContext of the View after sorting, but that also does not solve the issue.
Thanks.
Adding my own answer here.
So following the comments from the original post, #piofusco points out that a View does not update when an ObservableCollection has only been sorted. Even manually changing the collection (hence, raising NotifyPropertyChanged or NotifyCollectionChanged) does not update it.
Searching around a little more, I decided I could make use of CollectionViewSource, which would do my sorting for me - without changing the collection itself (hence allowing the control to retain its current scroll position). To get it working, basically, add a new property to the ViewModel of type CollectionViewSource, add a SortDescription, set its Source and bind directly to that property (instead of the original ObservableCollection:
In ViewModel:
private CollectionViewSource _sortedCollection = new CollectionViewSource();
public CollectionViewSource SortedCollection {
get {
_sortedCollection.Source = this.Items; // Set source to our original ObservableCollection
return _sortedCollection;
}
set {
if (value != _sortedCollection) {
_sortedCollection = value;
RaiseNotifyPropertyChanged("SortedCollection"); // MVVMLight ObservableObject
}
}
}
View XAML (note the binding to Property.View):
<ListBox ItemsSource="{Binding SortedCollection.View}" ... />
And in your View code-behind, if you have a Sort button:
ViewModel _vm = this.DataContext as ViewModel;
viewModel.SortedCollection.SortDescriptions.Clear(); // Clear all
viewModel.SortedCollection.SortDescriptions.Add(new SortDescription("PropertyName", ListSortDirection.Descending)); // Sort descending by "PropertyName"
And boom! Your sorted collection should update instantly in the View! Even better is that it retains our ObservableCollection functionality in that any updates to objects in the ObservableCollection will raise the NotifyPropertyChanged or NotifyCollectionChanged handlers, thereby updating the View (allowing for both sorting and updating of objects while retaining current scroll positions)!
Note: For those out there using a LongListSelector control, I wasn't able to get it to work, and with a little more internet-digging with I came across this post, which, discusses why LLS cannot bind to a CollectionViewSource.View without some modifications. So I ended up using a ListBox control instead. You can read about some of the differences here. For my task though, the ListBox will suffice.
I started implementing a MVVM design pattern in an existing WPF c# application. I am completely new and have never used design patterns or dependency injection before. I was looking at the frameworks already available and have adopted MVVM light. I moved the logic from the view to the viewmodel. I have lot of code in the PopulateTestMenu which is related to UI in the view model. It also has calls to the event handlers. How do I take care of this?
In the XAML I have:
<Window DataContext="{Binding Main, Source={StaticResource Locator}}">
<Menu>
<MenuItem Header="Load All History..." Command="{Binding LoadAllHistory}">
In the MainViewModel class I have:
public ICommand LoadAllHistory { get; private set; }
public MainViewModel()
{
LoadAllHistory = new RelayCommand(() => LoadHistoryExecute(), () => true);
}
The code that I moved from my view to the viewmodel:
private void LoadHistoryExecute()
{
try
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "Test History File (*.xml)|*.xml";
ofd.Title = "Open Test History";
ofd.Multiselect = true;
if (ofd.ShowDialog() == true)
{
ThreadPool.QueueUserWorkItem(LoadTestHistoryCallback, ofd.FileNames);
}
}
catch
{
//some code
}
}
private void LoadTestHistoryCallback(object state)
{
try
{
string[] fileNames = (string[])state;
foreach (string fileName in fileNames)
{
bool success = MyApp.Instance.ParseTestHistory(fileName);
string status = success
? String.Format("'{0}' loaded successfully.",
System.IO.Path.GetFileName(fileName))
: String.Format("Failed to load history from '{0}'.",
System.IO.Path.GetFileName(fileName));
Dispatcher.CurrentDispatcher.DynamicInvoke(delegate()
{
Status = status;
});
PopulateTestMenu(new SortedList<int, int>());
}
}
catch
{
//some code
}
}
private void PopulateTestMenu(SortedList<int, int> indexes)
{
try
{
_testMenuMutex.WaitOne();
//Populate the Tests menu with the list of tests.
Dispatcher.CurrentDispatcher.DynamicInvoke(delegate()
{
menuTests.Items.Clear();
var checkEventHandler = new RoutedEventHandler(testMenuItem_Checked);
bool added = false;
if (MyApp.Instance.TestHistory != null &&
MyApp.Instance.TestHistory.Count > 0)
{
List<ushort> subIds = new
List<ushort>MyApp.Instance.TestHistory.Keys);
foreach (ushort subId in subIds)
{
MenuItem menuItem = null;
menuItem = new MenuItem();
menuItem.Header = subId.ToString().PadLeft(5, '0');**
MenuItem none = new MenuItem();
none.Header = "None";
none.IsCheckable = true;
none.IsChecked = true;
none.Checked += checkEventHandler;
none.Unchecked += checkEventHandler;
menuItem.Items.Add(none);
if (MyApp.Instance.TestHistory != null &&
MyApp.Instance.TestHistory.ContainsKey(subId))
{
var tests = MyApp.Instance.TestHistory[subId];
if (tests != null)
{
foreach (Test t in tests)
{
MenuItem item = new MenuItem();
item.IsCheckable = true;
string description = t.Description.Replace("\n",
"\n".PadRight(34, ' '));
string header = abc;
item.Header = header;
item.DataContext = t;
item.Checked += checkEventHandler;
item.Unchecked += checkEventHandler;
menuItem.Items.Add(item);
}
if (tests.Count > 0)
{
menuTests.Items.Add(menuItem);
added = true;
}
}
}
// Carry over the previous selection.
if (indexes.ContainsKey(subId) && indexes[subId] > -1)
{ ((MenuItem)menuItem.Items[indexes[subId]]).IsChecked =
true;
}
}
}
I am still trying to figure out what you are asking =)...
But you are mixing up some things... Remember one of the core concepts of MVVM is to make the viewmodel testable and remove all view related code off from the viewmodel. So no dependencies to WPF at all. So MenuItem looks like a WPF MenuItem and should not be in your ViewModel.
Instead you could consider to make a MenuItemViewModel which binds to the MenuItem in the View. And it I could see an ObservableCollection<MenuItemViewModel> TestMenu instead of your sorted list.
In your method LoadTestHistoryCallback you would instanciate (could be done via DI) the MenuItemViewModel and add it to the TestMenu Collection. The MenuItemViewModel could have status property which could be assigned from outside or internaly. (It can also have some additional logic, hey its a viewmodel).
In the View you could then bind it to a list with a template representing the MenuItem via DataBinding.
<Menu DockPanel.Dock="Top" ItemsSource="{Binding Path=MenuItems}" />
So remember ViewModel can also contain ViewModels or collections of viewmodel.
Use the rich databinding api from WPF.
Work with bindable Properties like ObservebaleCollections or Properties that are extended with PropertyChanged notification.
HTH
P.S: You can then have a click ICommand in the MenuItemViewModel and execute actions or better use the EventAggregator or Messenger to notify other ViewModels ...(but that's a story for another question =)... )
You have applied the theory of MVVM correctly by moving that code to the ViewModel however just keep in mind that the View should only provide the "structure" of the display.
What is displayed is provided by the model in the ViewModel.
With that in mind separate out the menu parts from the ViewModel method and put them in the View, but leave the Test object creation parts (Binding ViewModel objects to View structure is what it's about).
Within your PopulateTestMenu method the menus and menu structure need to be specified in the View while the data populating them needs to be created and formatted in the ViewModel.
In the View you will bind the appropriate object parts to the menu structure, and the ViewModel will automatically fill it in with the model objects when the model is bound to the view.
Looking at the code, it appears that your Test object is your ViewModel, and the Menu and MenuItem structure needs to be created in the View, then you specify the binding of the specific properties of the Test object to the specific structure parts of the Menu within the View.
I'm still in the learning Phase of WPF, EF and MVVM and now I got the following problem. I can delete and insert new items in my DataGridView but I don't know how to update my items.
All I do is select an emptyrow which already has a primary key and then I put the data into it. It's working (updating database) but the GridView is not refreshing. I Need to restart the program first to see my updated data.
My Execute Command to Update my Database. I'm in the ViewModel class
public void ExecuteUpdate(object obj)
{
try
{
SelectedIndex.Child_Update(new Farbe { FarbauswahlNr = SelectedIndex.FarbauswahlNr, Kurztext = SelectedIndex.Kurztext, Ressource = SelectedIndex.Ressource, Vari1 = SelectedIndex.Vari1, Vari2 = SelectedIndex.Vari2 });
//ListeAktualisieren --> Refreshing the List
ListeAktualisieren();
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
}
Here is my Refresh Method which SHOULD Refresh the GridView. I'm in the ViewModel class
public void ListeAktualisieren()
{
farbliste.ListeAktualisieren(db);
farbliste.Model = farbliste.Model.Concat(farbliste.Addlist).ToList();
Model = farbliste.Model;
farbliste.Addlist.Clear();
}
The method is calling my Business List which also got a Refresh Method. Reading from my database here. I'm in the Business List class
public void ListeAktualisieren(TestDBEntities db)
{
Model.Clear();
foreach (var item in db.Farben)
{
//Insert and delete working
add = new Farbe { FarbauswahlNr = item.FarbauswahlNr, Kurztext = item.Kurztext, Ressource = item.Ressource, Vari1 = Convert.ToBoolean(item.Var1), Vari2 = item.Vari2 };
Addlist.Add(add);
}
}
Model is the Source of my GridView which is not Refreshing changed data when Updated but is showing new data rows when inserting or deleting.
You need Observablecollections and Classes with implemented INotifyPropertyChanged. Add the new element to the Observablecollection by insert and raise the event propertychanged by a change.
The rest should be done by WPF.
Edit: The Sourcecollection for the DataGrid needs to be the Observablecollection.
Edit2: To be nice I put the result of the comments here ;-)
Each row of the DataGrid is an element of the collection. Each cell of one row listens to a PropertyChangedEvent of its element (the String is Casesensitive so be carefull). If the getter of the property isn't called after the propertychangedevent the binding didn't receive the event.
This piece of Code can help asure that you don't call with nonexistent strings:
private void VerifyPropertyName(string PropertyName)
{
if (string.IsNullOrEmpty(PropertyName))
return;
if (TypeDescriptor.GetProperties(this)(PropertyName) == null) {
string msg = "Ungültiger PropertyName: " + PropertyName;
if (this.ThrowOnInvalidPropertyName) {
throw new isgException(msg);
} else {
Debug.Fail(msg);
}
}
}
Try adding this to your binding section
ItemsSource="{Binding Path=Model, UpdateSourceTrigger= PropertyChanged"}
I've scoured the internet and have yet to find a solution. Help me Stack-Overflow-Kenobi, you're my only hope!
So I've got this Silverlight application, right? And I have a combobox that's bound to a non-nullable database field so that it is populated on initialization with all possible values. And it's working fine in that regard.
However, when I SubmitChanges without having selected an item, no validation error is thrown, so my BindingValidationError handler is never activated. Now I would expect (and kinda need) an error to be thrown when pushing null into a non-nullable database column. That way the user knows to select an item.
When the value is not null, it is pushed to the database just fine. Basically, the binding works fine: I just need to know why the BindingValidationError handler is not hit. ToggleError needs to be run if no item is selected.
foo()
{
Binding databinding = new Binding(this.Id);
databinding.Source = bindingObject;
databinding.BindsDirectlyToSource = true;
databinding.Mode = BindingMode.TwoWay;
databinding.ValidatesOnDataErrors = true;
databinding.ValidatesOnExceptions = true;
databinding.ValidatesOnNotifyDataErrors = true;
databinding.NotifyOnValidationError = true;
databinding.UpdateSourceTrigger = UpdateSourceTrigger.Default;
CmbBox.DisplayMemberPath = _DisplayMemberPath;
CmbBox.SelectedValuePath = _SelectedValuePath;
CmbBox.SetBinding(ComboBox.SelectedItemProperty, databinding);
CmbBox.BindingValidationError += (sender, e) => ToggleError(e.Action == ValidationErrorEventAction.Added ? true : false , e.Error.ErrorContent.ToString());
}
private void ToggleError(bool enableError, string errorMessage)
{
hasError = enableError;
if (hasError)
{
CmbBox.Foreground = new SolidColorBrush(Utilities.DarkRed);
Error.Visibility = Visibility.Visible;
this.errorMessage = errorMessage;
}
else
{
CmbBox.Foreground = new SolidColorBrush(Utilities.DarkGreen);
Error.Visibility = Visibility.Collapsed;
errorMessage = null;
}
}
Thank in advance : )
Cameron
The BindingValidationError event fires when a TwoWay Binding is updated and the setter throws an exception. If you never select a value for the ComboBox then the Binding is never updated and it will never throw an error. You need to do validation yourself before you call SubmitChanges.
If you are using Silverlight 4 you may want to look into using INotifyDataErrorInfo to do validation in your code and then update the UI to show the validation error.