There is a page where the user selects parameters to show the proper collection then on button click jumps to the next page (Coll) where it should show up.
User Selection Page XAML:
<ContentPage.BindingContext><xyz:UserSelectionViewModel</ContentPage.BindingContext>
...
<Button x:Name="Start" Command="{Binding LoadData}" Pressed="StartClick"/>
User Selection Page C#:
private async void ButtonClick(object sender, EventArgs e)
{
var vm = (CollViewModel)BindingContext;
vm.Hard = HardButtonSelected == Hard;
...
vm.Subject = vm.Subject.ToLower();
}
UserSelectionViewModel:
public class UserSelectionViewModel : BaseViewModel
{
public UserSelectionViewModel()
{
_dataStore = DependencyService.Get<IDataStore>();
_pageService = DependencyService.Get<IPageService>();
LoadData= new AsyncAwaitBestPractices.MVVM.AsyncCommand(FilterData);
FilteredData = new ObservableRangeCollection<Items>();
}
public async Task FilterData()
{
FilteredData.Clear();
var filtereddata = await _dataStore.SearchData(Hard, Subject).ConfigureAwait(false);
FilteredData.AddRange(filtereddata);
OnPropertyChanged("FilteredData");
Debug.WriteLine(FilteredData.Count());
await Device.InvokeOnMainThreadAsync(() => _pageService.PushAsync(new Coll(FilteredData)));
}
}
Coll XAML:
<ContentPage.BindingContext><xyz:CollViewModel</ContentPage.BindingContext>
...
<CarouselView ItemsSource="{Binding Source={RelativeSource AncestorType={x:Type z:Coll}}, Path=InheritedData}" ItemTemplate="{StaticResource CollTemplateSelector}">
...
Coll C#:
public partial class Coll : ContentPage
{
public ObservableRangeCollection<Feladatok> InheritedData { get; set; }
public Coll(ObservableRangeCollection<Feladatok> x)
{
InitializeComponent();
InheritedData = x;
OnPropertyChanged(nameof(InheritedData));
}
}
CollViewModel:
public class CollViewModel : UserSelectionViewModel { ... }
BaseViewModel:
private ObservableRangeCollection<Feladatok> inheriteddata;
public ObservableRangeCollection<Feladatok> InheritedData
{
get
{
return inheriteddata;
}
set
{
if (value != inheriteddata)
{
inheriteddata = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("InheritedData"));
}
}
}
Managed to make it work like this with the help of Jason's tips. My only concern remaining is that: Won't this slow down the page that I load the observable collection two times basically? Is it a good practice as I have made it?
Eventually set the BindingContext to the VM and Binding from there. I still feel like it could be done more efficently or maybe that's how it is done. ViewModels are still new for me and I feel like it's much more code and slower with it. But I will close this, as it is working now.
Related
I have to show a list where each item will be validated. I am subscribed to Validation.ErrorEvent on a top level to monitor for children.
When I remove item with validation error from list this event is not rised.
In example below I have 3 TextBox on screen, each is bound to int property. Entering wrong value will fire event (Title is changed to "+"), fixing value afterwards will fire event once (Title is changed to "-").
However removing TextBox while having error will not rise event (to clean up) and Title stay "+":
How can I fix that? Ideally I want that this event is automatically rised before removing happens.
Please note: in real project there is complex hierarchy of view models, solutions like "set Title in delete method" would require monitoring for sub-views and propagating that info through all hierarchy, which I'd like to avoid. I'd prefer view-only solution.
MCVE:
public partial class MainWindow : Window
{
public ObservableCollection<VM> Items { get; } = new ObservableCollection<VM> { new VM(), new VM(), new VM() };
public MainWindow()
{
InitializeComponent();
AddHandler(Validation.ErrorEvent, new RoutedEventHandler((s, e) =>
Title = ((ValidationErrorEventArgs)e).Action == ValidationErrorEventAction.Added ? "+" : "-"));
DataContext = this;
}
void Button_Click(object sender, RoutedEventArgs e) => Items.RemoveAt(0);
}
public class VM
{
public int Test { get; set; }
}
xaml:
<StackPanel>
<ItemsControl ItemsSource="{Binding Items}" Height="200">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Test, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Remove first" Click="Button_Click" />
</StackPanel>
After a little time, I have a working solution that you could start with. As already mentioned by Ed, You're using the UI as a data structure, which is never a good idea. The MVVM way to do validation is IDataErrorInfo, this is true and really you should be implementing the IDataErrorInfo interface to handle these errors.
On another note, here's what I did to get it working. I am handling the CollectionChanged event for the ObservableCollection of your VM's. When the collection changes, you need to find the element that was actually being removed, if found, we can try and clear it's ValidationError object for that element itself.
Here is the class -
public partial class MainWindow : Window
{
public ObservableCollection<VM> Items { get; } = new ObservableCollection<VM> { new VM(), new VM(), new VM() };
public MainWindow()
{
InitializeComponent();
Items.CollectionChanged += Items_CollectionChanged;
AddHandler(Validation.ErrorEvent, new RoutedEventHandler((s, e) =>
Title = ((ValidationErrorEventArgs)e).Action == ValidationErrorEventAction.Added ? "+" : "-"));
DataContext = this;
}
private void Items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) {
foreach (TextBox tb in FindVisualChildren<TextBox>(this))
{
if(tb.DataContext == e.OldItems[0])
{
Validation.ClearInvalid(tb.GetBindingExpression(TextBox.TextProperty));
break;
}
}
}
}
private void Button_Click(object sender, RoutedEventArgs e) => Items.RemoveAt(0);
public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
}
foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
}
public class VM
{
public int Test { get; set; }
}
The bread and butter to make this work is Validation.ClearInvalid(tb.GetBindingExpression(TextBox.TextProperty)); which actually removes all ValidationError objects from the BindingExpressionBase object which in this case is TextBox.TextProperty.
Note: There has been no error checking here, you may want to do that.
My goal is to output a list in a datagrid, but this doesn't work and the datagrid is empty.
I tried to display the list in an other way and it did (but I can't remember what it was) and it worked, except for it not being in a datagrid but just data. I have changed up some things, but back then it reached the end and got displayed.
ViewModel in Mainwindow:
public class ViewModel
{
public List<ssearch> Items { get; set; }
private static ViewModel _instance = new ViewModel();
public static ViewModel Instance { get { return _instance; } }
}
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
//For simplicity, let's say this window opens right away
var Mdata = new MDataWindow { DataContext = DataContext };
Mdata.Show();
}
Other Window for data display:
string searchParam = "status = 1";
public MDataWindow()
{
InitializeComponent();
}
private void AButton_Click(object sender, RoutedEventArgs e)
{
MainWindow.ViewModel.Instance.Items = Search(searchParam);
}
public List<ssearch> Search(string where)
{
{
//Lots of stuff going on here
}
return returnList;
}
And in WPF:
<Window x:Class="WPFClient.MDataWindow"
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:WPFClient"
mc:Ignorable="d"
Title="MDataWindow" Height="Auto" Width="Auto">
<StackPanel>
<Button x:Name="AButton" Click="AButton_Click" Content="Load" />
<DataGrid ItemsSource="{Binding Items}" />
</StackPanel>
</Window>
I have no clue where the error is and tried to strip the code down as much as possible without killing error sources. The Datagrid just stays empty when I press the "Load" button.
EDIT:
I tried to convert the list into an observableColletion before passing it to the ViewModel, but this didn't work. I am working with a library, which I am not sure how to use observableCollection with, so I converted it instead of using it right away:
VM:
public ObservableCollection<Product> Items { get; set; }
Data Window:
List<Product> pp = Search_Products(searchParam);
var oc = new ObservableCollection<Product>(pp);
MainWindow.ViewModel.Instance.Items = oc;
First, change your List<Product> to an ObservableCollection<Product> as this will help to display the Items of the list on Add/Remove immediately.
This is because ObservableCollection implements the INotifyCollectionChanged interface to notify your target(DataGrid) to which it is bound, to update its UI.
Second, your binding can never work as expected due to changed reference of your collection.
private void AButton_Click(object sender, RoutedEventArgs e)
{
// You are changing your Items' reference completely here, the XAML binding
// in your View is still bound to the old reference, that is why you're seeing nothing.
//MainWindow.ViewModel.Instance.Items = Search(searchParam);
var searchResults = Search(searchParam);
foreach(var searchResult in searchResults)
{
MainWindow.ViewModel.Instance.Items.Add(searchResult);
}
}
Make sure you have changed the List to ObservableCollection upon running the Add loop, else you will get an exception saying the item collection state is inconsistent.
The ViewModel class should implement the INotifyPropertyChanged interface and raise its PropertyChanged event whenever Items is set to a new collection:
public class ViewModel : INotifyPropertyChanged
{
private List<ssearch> _items;
public List<ssearch> Items
{
get { return _items; }
set { _items = value; OnPropertyChanged(); }
}
private static ViewModel _instance = new ViewModel();
public static ViewModel Instance { get { return _instance; } }
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
This is required to notify the view regardless of the type of Items.
If you change the type of Items to ObservableCollection<T>, you should initialize the collection in the view model once:
public class ViewModel
{
public ObservableCollection<ssearch> Items { get; } = new ObservableCollection<ssearch>();
private static ViewModel _instance = new ViewModel();
public static ViewModel Instance { get { return _instance; } }
}
...and then add items to this collection instead of setting the property to a new one:
private void AButton_Click(object sender, RoutedEventArgs e)
{
MainWindow.ViewModel.Instance.Items.Clear();
var search = Search(searchParam);
if (search != null)
foreach (var x in search)
MainWindow.ViewModel.Instance.Items.Add(x);
}
I have the following problem:
in the application, when the user returns to the computer, I have to process the event and show him a modal window in which to offer 3 possible answers.
I do this using the service:
public WinUserReturnDialogServiceImpl(ISettingsManager
settingsManager) : base(settingsManager)
{
_dialogPage = new UserReturnDialogPage();
_dialogPage.AddButton.Click += OnAddButtonClick;
_dialogPage.DivideButton.Click += OnDivideButtonClick;
_dialogPage.CancelButton.Click += OnCancelButtonClick;
_dialogWindow = new DialogWindow()
{
Content = _dialogPage
};
}
protected override void ShowCustomDialog(UserReturnDialogData dialogData)
{
_pauseDuration = Math.Floor(dialogData._userAfkMinuites);
Application.Current.Dispatcher.Invoke(() =>
{
_dialogPage.AFKMessage.Text = string.Format("Вы отсутствовали {0} мин", _pauseDuration);
_dialogWindow.Show();
});
return;
}
private void OnAddButtonClick(object sender, RoutedEventArgs e)
{
CloseCustomDialog();
}
Page Code behind:
//[MvxViewFor(typeof(UserReturnDialogViewModel))]
//[WinContentPresentation(IsSheet = true, TransitionForwardType = TransitionType.ToRight, TransitionReturnType = TransitionType.FromRigth, WindowIdentifier = nameof(DialogWindow))]
[MvxContentPresentation(WindowIdentifier = nameof(DialogWindow), StackNavigation = false)]
public partial class UserReturnDialogPage : MvxWpfView<UserReturnDialogViewModel>
{
public UserReturnDialogPage()
{
InitializeComponent();
}
and page xaml:
<views:MvxWpfView
x:Class="SmlHours.Win.Presentation.Views.Pages.Dialogs.UserReturnDialogPage"
x:TypeArguments="vm:UserReturnDialogViewModel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:MvvmCross.Platforms.Wpf.Views;assembly=MvvmCross.Platforms.Wpf"
xmlns:vm="clr-namespace:SmlHours.Core.Presentation.ViewModels.Dialogs;assembly=SmlHours.Core"
...
<Button
x:Name="AddButton"
HorizontalAlignment="Left"
Visibility="Visible"
Width="85"
Height="32"
Template="{DynamicResource BaseNavigationButtonTemplate}"
Command="{Binding AddTimeAfterUserReturn}"
ViewModel:
public class UserReturnDialogViewModel : BaseViewModel
{
private IMonitoringInteractor _monitoringInteractor;
public IMvxCommand AddTimeAfterUserReturn { get; private set; }
public UserReturnDialogViewModel(IMonitoringInteractor monitoringInteractor)
{
_monitoringInteractor = monitoringInteractor;
AddTimeAfterUserReturn = CreateAsyncCommand(AddTimeAfterReturnTask);
}
//I need to fire this command after button pressed!!!
private Task AddTimeAfterReturnTask() => Task.Run(async () =>
{
var model = _monitoringInteractor.FixationTimeAfterUserReturn();
});
}
The event is triggered, the page is displayed, but the viewmodel is not attached to the page and does not respond to button presses. However, the OnAddButtonClick, etc. service commands that close the window react to pressing the buttons.
How do I bind view and viewmodel, so that pressing the buttons works in the viewmodel?
Thanks a lot to those who responded
Problem was that I try to create new Window/Page by manual. For binding vm and view should use ViewDispatcher:
var ViewDispatcher = Mvx.Resolve<IMvxViewDispatcher>();
ViewDispatcher.ShowViewModel(newMvxViewModelRequest(typeof(UserReturnDialogViewModel)));
I have spent a couple hours trying to figure out this ONE problem. Here's what is happening:
I am trying to bind a Title to my XAML file from my ViewModel. All of the code executes (I checked using breakpoints/watch), but the binding doesn't actually work. I am very new to development and especially MVVM, so I am having a hard time figuring this out. Relevant code:
App.Xaml.Cs
private static MainPageViewModel _mainPageViewModel = null;
public static MainPageViewModel MainPageViewModel
{
get
{
if (_mainPageViewModel == null)
{
_mainPageViewModel = new MainPageViewModel();
}
return _mainPageViewModel;
}
}
MainPageModel
public class MainPageModel : BaseModel
{
private string _pageTitle;
public string PageTitle
{
get { return _pageTitle; }
set
{
if (_pageTitle != value)
{
NotifyPropertyChanging();
_pageTitle = value;
NotifyPropertyChanged();
}
}
}
MainPageViewModel
private void LoadAll()
{
var page = new MainPageModel();
page.PageTitle = "title";
MainPageViewModel
public MainPageViewModel()
{
LoadAll();
}
MainPage.Xaml.Cs
public MainPage()
{
InitializeComponent();
DataContext = App.MainPageViewModel;
}
MainPage.Xaml
<Grid x:Name="LayoutRoot">
<phone:Panorama Title="{Binding PageTitle}">
Do I need a using statement in the Xaml too? I thought I just needed to set the data context in the MainPage.Xaml.Cs file.
I'm pretty sure I've posted all of the relevant code for this. Thanks everyone!!
The problem is here, in the view model class:
private void LoadAll()
{
var page = new MainPageModel();
page.PageTitle = "title";
All you've done here is create a local object "page" -- this will not be accessible anywhere outside the local scope. I suppose what you meant to do is make "page" a member of "MainPageViewModel":
public class MainPageViewModel
{
public MainPageModel Model { get; private set; }
private void LoadAll()
{
_page = new MainPageModel();
_page.PageTitle = "title";
}
}
This way, you'll be able to bind to the "PageTitle" property -- but remember, it's a nested property, so you'll need:
<phone:Panorama Title="{Binding Model.PageTitle}">
I'm currently working on Windows Phone applications and I would like to use Reactive Extensions to create asynchronism to have a better UI experience.
I use the MVVM pattern: my View has a ListBox binded in my ViewModel to an ObservableCollection of Items. An Item has traditional properties like Name or IsSelected.
<ListBox SelectionMode="Multiple" ItemsSource="{Binding Checklist, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Content="{Binding Name}" IsChecked="{Binding IsSelected, Mode=TwoWay}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I instantiate my MainViewModel in App.xaml.cs:
public partial class App : Application
{
private static MainViewModel _viewModel = null;
public static MainViewModel ViewModel
{
get
{
if (_viewModel == null)
_viewModel = new MainViewModel();
return _viewModel;
}
}
[...]
}
And I load my data in MainPage_Loaded.
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
this.DataContext = App.ViewModel;
this.Loaded += new System.Windows.RoutedEventHandler(MainPage_Loaded);
}
private void MainPage_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
App.ViewModel.LoadData();
}
}
I load my data from my ViewModel (I'm going to move that code to the Model later):
public class MainViewModel : ViewModelBase
{
public ObservableCollection<Item> _checklist;
public ObservableCollection<Item> Checklist
{
get
{
return this._checklist;
}
set
{
if (this._checklist != value)
{
this._checklist = value;
}
}
}
private const string _connectionString = #"isostore:/ItemDB.sdf";
public void LoadData()
{
using (ItemDataContext context = new ItemDataContext(_connectionString))
{
if (!context.DatabaseExists())
{
// Create database if it doesn't exist
context.CreateDatabase();
}
if (context.Items.Count() == 0)
{
[Read my data in a XML file for example]
// Save changes to the database
context.SubmitChanges();
}
var contextItems = from i in context.Items
select i;
foreach (Item it in contextItems)
{
this.Checklist.Add(it);
}
}
}
[...]
}
And it works fine, items are updated in the View.
Now I want to create asynchronism. With a traditional BeginInvoke in a new method that I call from the View instead of LoadData it works fine.
public partial class MainPage : PhoneApplicationPage
{
[...]
private void MainPage_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
App.ViewModel.GetData();
}
}
I use a property that I called CurrentDispatcher, it is filled in the App.xaml.cs with App.Current.RootVisual.Dispatcher.
public class MainViewModel : ViewModelBase
{
[...]
public Dispatcher CurrentDispatcher { get; set; }
public void GetData()
{
this.CurrentDispatcher.BeginInvoke(new Action(LoadData));
}
[...]
}
But I would like to use Reactive Extentions. So I tried different elements of Rx like ToAsync or ToObservable for example but I had some "UnauthorizedAccessException was unhandled" with "Invalid cross-thread access" when I add an item to the Checklist.
I tried to ObserveOn other threads cause maybe the error comes from mix between the UI and the background threads but it doesn't work. Maybe I don't use Rx like I would be in that particular case?
Any help would be much appreciate.
EDIT after your answers:
Here is a code which works great!
public void GetData()
{
Observable.Start(() => LoadData())
.ObserveOnDispatcher()
.Subscribe(list =>
{
foreach (Item it in list)
{
this.Checklist.Add(it);
}
});
}
public ObservableCollection<Item> LoadData()
{
var results = new ObservableCollection<Item>();
using (ItemDataContext context = new ItemDataContext(_connectionString))
{
//Loading
var contextItems = from i in context.Items
select i;
foreach (Item it in contextItems)
{
results.Add(it);
}
}
return results;
}
As you see I didn't use correctly before. Now I can use a ObservableCollection and use it in the Susbscribe. It's perfect! Thanks a lot!
There appears to be no asynchronicity in your code (save the BeginInvoke, which I dont think is doing what you think it is doing).
I assume that you want to use Rx because your code is all blocking at the moment and the UI is unresponsive while the connection to the database is made and the data is loaded :-(
What you want to do is perform the "heavy lifting" on a background thread and then once you have the values just add them to the ObservableCollection on the Dispatcher. You can do this one of three ways:
Return the entire collection in one go. You then loop through the list with a foreach loop adding to the ObservableCollection. This has the potential downside of blocking the UI (unresponsive app) if the list is too large
Return the collection one value at a time, adding each item to the ObservableCollection in an independent call to the dispatcher. This will keep the UI responsive but can take much longer to complete
Return the collection in buffered chunks and try to get the best of both worlds
The code you want may look like this
public IList<Item> FetchData()
{
using (ItemDataContext context = new ItemDataContext(_connectionString))
{
//....
var results = new List<Item>();
foreach (Item it in contextItems)
{
results.Add(it);
}
return results;
}
}
public void LoadData()
{
Observable.Start(()=>FetchData())
.ObserveOnDispatcher()
.Subscribe(list=>
{
foreach (Item it in contextItems)
{
this.Checklist.Add(it);
}
});
}
The code ain't ideal for Unit testing, but it appears that this is not of interest to you any way (static members, VM with DB connectsions etc..) so this might just work?!
I just ran into this myself. There are multiple ways to get to the dispatcher, I had to use the following. I call this directly from my ViewModels (no passing in from outside).
Application.Current.Dispatcher.Invoke(action);