I have an application with user-login which representates a "lifetime" object till the user logs out. I'm using MVVM Light but don't know how to use the SimpleIoC (with the Messenger) correctly.
Currently I save the object in the MainViewModel as CurrentUser Property of my custom User object and pass it to other ViewModels via constructor and save it there as a Property too. But with that I have the issue, that at some places the object gets overwritten / created new.
This is probably the wrong way to do it as i figured out that the SimpleIoC can simply return the registered instances with GetInstance<>() and if needed a Key.
So the right way is it to create a User object with Key to get it in needed classes, modify and save it so the other classes can get the updated value when calling (or PropertyChanged possible?), right?
I tried to create it like that, but the object is obviously not updated:
public MainViewModel()
{
RegisterMessenger();
CurrentUser = SimpleIoc.Default.GetInstance<User>("123");
_dataService = SimpleIoc.Default.GetInstance<IDataService>();
Messenger.Default.Send(new NotificationMessage("GoToLoginPage"));
}
public User CurrentUser
{
get { return _currentUser; }
set { Set(ref _currentUser, value); }
}
public User CurrentPageViewModel
{
get { return _currenPageViewModel; }
set { Set(ref _currentPageViewModel, value); }
}
public void RegisterMessenger()
{
Messenger.Default.Register<NotificationMessage>(this, message =>
{
switch (message.Notification)
{
case "GoToLoginPage":
if (_loginViewModel == null)
_loginViewModel = new LoginViewModel(_dataService);
CurrentPageViewModel = _loginViewModel;
break;
// Other cases...
});
}
}
// -------------------------------------------------------------------------
public LoginViewModel(IDataService dataService)
{
UserObj = SimpleIoc.Default.GetInstance<User>("123");
_dataService = dataService;
LoginCommand = new RelayCommand(LoginExecute, LoginCanExecute);
}
Would it also be better, if I call the ViewModels with GetInstance<SomeViewModel>().. but then, how I pass paramters to them and let them stay alive / recreate / destroy?
Would be nice if someone can point me out the right way, thanks!
Related
I'm using MVVM in a Xamarin application, I have an interface to navigate between pages:
public interface INavigate
{
INavigate Next();
INavigate Previous();
string ViewTitle { get; }
}
In the implementing views:
public partial class V2Upload : ContentView, INavigate
{
public string ViewTitle => "Upload photos";
public INavigate Next()
=> new V3AdDetail();
public INavigate Previous()
=> new V1Agreement();
}
and in the view model
I have a property of type INavigate:
public INavigate CurrentAddItemStep
{
get { return _currentAddItemStep; }
set { Set(ref _currentAddItemStep, value); }
}
and the Content property of the parent view is bound to this property:
when next button is clicked I execute this code:
CurrentAddItemStep = CurrentAddItemStep.Next();
ViewTitle = CurrentAddItemStep.ViewTitle;
now a validation method is required before navigating to the next page for all the Content views..
I want to keep the MVVM pattern as clean as possible by not writing business code in the view, for example in the V2Upload view the File1 and File2 properties of the view model shouldn't be null:
private bool ValidateFiles(){
return (File1 ?? File2) != null;
}
but since the navigating is done dynamically in run-time, I can't know which view is the current view.
I'm thinking to use reflection , to know what is the name of the view (but this will break the whole design)
Another option is to provide a function parameter to the Next method, but also how to provide it in the design time from the view model?
This is what I'm doing now:
public INavigate Next()
{
if (((ViewModel.AddItemViewModel)BindingContext).ValidateFiles())
return new V3AdDetail();
else
return this;
}
but again, I'm accessing the view model from the view (and had to change the ValidateFiles method from private to public), which I want to avoid
I'm using SQLite in my application to store some Message objects. I display a list of Message items in "View 1". When I change a property in the Edit View "View 2", I want the property to also change in the list.
Classes
Message.cs
class Message : INotifyPropertyChanged
{
private uint _id;
public uint Id
{
get
{
return _id;
}
set
{
// Trigger INotifyPropertyChanged
Set("Id", ref _id, value);
}
}
private string _content;
public string Content
{
get
{
return _content;
}
set
{
// Trigger INotifyPropertyChanged
Set("Content", ref _content, value);
}
}
...
}
MessageViewModel.cs
class MessageViewModel : INotifyPropertyChanged
{
private Message message;
...
private string _content;
public string Content
{
get
{
return message.Content;
}
set
{
// Set value
message.Content = value;
// Trigger INotifyPropertyChanged
RaisePropertyChanged();
}
}
...
}
View 1
View1.xaml
The datacontext is View1ViewModel
View1ViewModel.cs
private List<MessageViewModel> _messages;
public List<MessageViewModel> Messages
{
get
{
return _messages;
}
set
{
// Trigger INotifyPropertyChanged
Set("Messages", ref _messages, value);
}
}
...
private async void loadMessages()
{
// Get the messages from SQLite database
var messages = await newMessages();
Messages = new MessageViewModelCollection(messages);
}
View 2
View2ViewModel.cs
private MessageViewModel _message;
public MessageViewModel Message
{
get
{
return _message;
}
set
{
// Trigger INotifyPropertyChanged
Set("Message", ref _message, value);
}
}
...
private async void loadMessage()
{
// Get the message from SQLite database by Id
var message = await newMessage(messageId);
Message = new MessageViewModel(message);
}
The functions newMessages and newMessage(uint messageId) return new Message objects from the database.
I normally use the INotifyPropertyChanged implementation, but this doesn't work. I query the database 2 times, once for the list (View 1), and once for the edit page (View 2). The SQLite returns two different copies of the Message object, therefore the INotifyPropertyChanged meganism will not work (Only in the current page, not the pages in the backstack).
I could fix the problem by re-using the Message item from the list, but I cannot always do this in all views.
Is there a way to make the INotifyPropertyChanged work in this scenario? Or do I need a different approach to update the values?
You need a different approach. IMHO, the best thing would be to separate the data model from the view model. So have a MessageVm that binds to the WPF view, and put the logic into it to update itself appropriately (along with firing any necessary INotifyPropertyChanged's) from the Message objects that get passed into it.
There are some automatic mapping tools like Automapper which might, to some degree, alleviate the pain that comes along with this approach.
But you really should separate the view model from the data model, the decoupling of these layers is one of the principle tenets of WPF programming.
I'm trying to put an universal app together and I'm using mvvm light but I'm getting the following error when compiling my app:
Error 1 Type not found in cache: MyApp.Model.LocationModel
...\MyApp.WindowsPhone\Views\LocationPage.xaml 10 5 MyApp.WindowsPhone
It does compile successfully but I can't figure out what's causing the problem. I've found a couple of article on stackoverflow:
SimpleIoC - Type not found in cache: Windows.UI.Xaml.Controls.Frame
MVVM Light “Type Not Found in cache”
But neither one apply to my problem. The first thing I've noticed is that the error is somehow displaying a Model where the problem resides rather than a ViewModel.
Error 1 Type not found in cache: MyApp.Model.LocationModel.
...\MyApp\MyApp.WindowsPhone\Views\LocationPage.xaml 10 5 MyApp.WindowsPhone
The error in my xaml occurs on the line where I defined my DataContext:
<Page
....
DataContext="{Binding Source={StaticResource Locator}, Path=LocationViewModel}">
My LocationViewModel class is defined as follows:
public class LocationViewModel : ViewModelBase
{
private RelayCommand _saveCommand;
private RelayCommand _cancelCommand;
#region Properties
public int Id
{
get
{
return this.Location.Id;
}
}
public string Title
{
get
{
return this.Location.Title;
}
}
public string Description
{
get
{
return this.Location.Description;
}
}
public string CreatedDateFormatted
{
get
{
return this.Location.CreatedDate.ToString("d");
}
}
public string LastUpdatedDateFormatted
{
get
{
return Location.LastUpdatedDate.ToString("d");
}
}
public string ImagePath
{
get
{
return this.Location.ImagePath;
}
}
public LocationModel Location
{
get;
private set;
}
#endregion
#region Constructors
public LocationViewModel(LocationModel model)
{
this.Location = model;
this.Location.PropertyChanged += (s, e) =>
{
if (e.PropertyName == LocationModel.DescriptionPropertyName)
{
RaisePropertyChanged(() => Description);
}
if (e.PropertyName == LocationModel.TitlePropertyName)
{
RaisePropertyChanged(() => Title);
}
if (e.PropertyName == LocationModel.ImagePathPropertyName)
{
RaisePropertyChanged(() => ImagePath);
}
if (e.PropertyName == LocationModel.CreatedDateStringPropertyName)
{
RaisePropertyChanged(() => CreatedDateFormatted);
}
if (e.PropertyName == LocationModel.LastUpdatedDateStringPropertyName)
{
RaisePropertyChanged(() => LastUpdatedDateFormatted);
}
};
}
#endregion
public RelayCommand SaveCommand
{
get
{
return this._saveCommand ?? (this._saveCommand = new RelayCommand(ExecuteSaveCommand));
}
}
public RelayCommand CancelCommand
{
get
{
return this._cancelCommand ?? (this._cancelCommand = new RelayCommand(ExecuteCancelCommand));
}
}
private void ExecuteSaveCommand()
{
}
private void ExecuteCancelCommand()
{
}
}
and my property for my LocationViewModel is defined as follows in my ViewModelLocator class:
public LocationViewModel LocationViewModel
{
get
{
return ServiceLocator.Current.GetInstance<LocationViewModel>();
}
}
and is registered in the ViewModelLocator's constructor:
SimpleIoc.Default.Register<LocationViewModel>();
and when this code is called, it registers my LocationViewModel correctly.
When click on my "add" button, it navigate to the page where the LocationViewModel is set as the DataContext and the error occurs at run-time.
The code I'm calling from LocationsViewModel (not LocationViewModel) that's calling the navigation is:
private void ExecuteAddCommand()
{
_navigationService.Navigate(typeof(LocationPage));
}
When debugging the above, it creates the LocationPage, followed by calling the LocationViewModel from the ViewModelLocator and this is when the same error occurs but at run-time i.e.
return ServiceLocator.Current.GetInstance<LocationViewModel>();
When I move my mouse over the , it displays the following:
Message: "Type not found in cache: MyApp.Model.LocationModel."
InnerException: at GalaSoft.MvvmLight.Ioc.SimpleIoc.DoGetService
(Type serviceType, String key) at
GalaSoft.MvvmLight.Ioc.SimpleIoc.GetInstance[TService]()
at Inventory.ViewModel.ViewModelLocator.get_LocationViewModel()
Actually, I've just realized that the error is generated much earlier but no error is thrown. It is actually generated when registering the LocationViewModel in the constructor of ViewModelLocator:
SimpleIoc.Default.Register<LocationViewModel>();
Any ideas?
Thanks.
The LocationViewModel constructor has dependency on LocationModel. The SimpleIoc container couldn't create the view model instance as the constructor requires a LocationModel object which you can't pass directly. You can probably use MVVMLight Messenger to decouple the LocationModel object from the LocationViewModel constructor.
public LocationViewModel()
{
MessengerInstance.Register<LocationModel>(this,m=>{model=m;
//PropertyChanged code
});
}
In the LocationsViewModel, send the LocationModel object you wanted to use in the LocationViewModel constructor by just sending it.
public void ExecuteAddCommand()
{
MessengerInstance.Send<LocationModel>(LocationModelObj);
_navigationService.navigate(tyepof(LocationPage));
}
For this to succeed though, you'd need to register LocationViewModel to register to receive LocationModel object before sending the object from LocationsViewModel. So, you need to create your view model immediately by using an overload of SimpleIoc's Register method.
SimpleIoc.Default.Register<LocationViewModel>(true);
Based on what #Shridhar said The SimpleIoc container couldn't create the view model instance as the constructor requires a LocationModel object which you can't pass directly, I thought I'd try adding a parameterless constructor but I got another error i.e.
Cannot register: Multiple constructors found in LocationViewModel but
none marked with PreferredConstructor.
So I marked my parameterless constructor with the PreferredConstructor as such:
[PreferredConstructor]
public LocationViewModel()
{
}
This sorted my problem but as mentioned to #Shridar, I'm not sure whether or not this is the correct solution so I will spend more time investigating and see if this works as expected and doesn't have any side effects.
I'll update as soon as I have something.
I also experienced a similar error while trying to use MVVMLight DialogService; the solution was to make sure it is registered in the ViewModelLocator
public ViewModelLocator()
{
SimpleIoc.Default.Register<IDialogService, DialogService>();
}
I am using MVVM (prism) to develop wpf application.
One of my model class "StandardContact" has its properties directly bound to the view. I use IDataErrorInfo to track and notify whether the model has any error. If there are any errors in Model, I disable the "Save" Command.
As the user enters some data, I use the StandardContact.PropertyChanged handler to see if "Save" command can execute (i.e if the model data entered by user is valid). The problem is that the StandardContact.PropertyChanged handler is called before the IDataErrorInfo's validation code, so CanExecute for "Save" command does not correctly reflect whether the command can be executed or not. What I am looking for is that, before the CanExecute executes, the IDataErrorInfo validation should run so that the CanExecute will query on the latest data in model and decide whether it is enabled or not. Here is the sample code that I am using
Model:
public class StandardContact :EntityBase, IDataErrorInfo
{
public virtual string Name
{
get { return _name; }
set { SetField(ref _name, value, () => Name); }
}
//...
//Validators
public string this[string propertyName]
{
get
{
string error = null;
//....
}
ViewModel
public class SContactEditViewModel : NotificationObject, INavigationAware
{
//....
StandardContact.PropertyChanged +=
new PropertyChangedEventHandler(StandardContact_PropertyChanged);
void StandardContact_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//Requery if command can execute
SaveNewCommand.RaiseCanExecuteChanged();
}
}
I just inspected our priprietary MVVM library. Inside the ViewModels indexer (in your case this is the Models indexer) the requested Property is validated:
public string this[string propertyName]
{
get
{
string result = null;
if (CanDataErrorValidated(propertyName))
{
int errorCount = CurrentValidationAdapter.ErrorCount();
result = ValidateProperty(propertyName, GetValidateValue(propertyName));
// if the error flag has been changed after validation
if (errorCount != CurrentValidationAdapter.ErrorCount())
{
RaisePropertyChanged(PropHasError);
RaisePropertyChanged(PropError);
}
}
else
{
RaisePropertyChanged(PropHasError);
RaisePropertyChanged(PropError);
}
return result;
}
}
So the solution of your problem seems to validate the requested property on the fly.
I don't use prism, but if it exposes some sort of IsValid method or property you can use that to trigger your error checking. And if it doesn't you can write your own.
The basic idea without prism is to have to leverage IDataErrorInfo.Error by doing
bool IsValid{ get{return string.IsNullOrEmpty(Error) } // trigger validation
Then inside your Save.CanExecute method
return IsValid; // trigger validation on demand
HTH,
Berryl
On the main window onClick I have
AddNoticeAboutWrongCity addNoticeAboutWrongCity = new AddNoticeAboutWrongCity();
addNoticeAboutWrongCity.DataContext = ((VerificationViewModule)this.DataContext).WrongCityNotice;
addNoticeAboutWrongCity.ShowDialog();
At popup window there a lot of textboxes and two buttons
Delete object:
this.DataContext = null;
And second option "Save edited notice" which is not usable , because every change of user affection datacontext on main window,and this is demand from design department :)
I don't know why first option(it's "implementation" doesn't work.
Second explanation:
On the ParentWindow I have list of Notices and I can click EditSelectedNotice.
On the EditNoticeWindow I can edit Notice or delete Notice.
Editinig works(After closing EditNoticeWindow I see changed notice on the ParentWindow), but deleting doesn't (Notice is still in collection - on control and in this.DataContext)
My ViewModel:
class VerificationViewModule
{
public ObservableCollection<ReporterNotice> ReporterNotices { get; set; }
public ReporterNotice OtherNotice
{
get
{
return ReporterNotices.Where(n => n.Type == ReporterNoticeType.Other).FirstOrDefault();
}
}
public ReporterNotice DuplicateNotice
{
get
{
return ReporterNotices.Where(n => n.Type == ReporterNoticeType.Duplicate).FirstOrDefault();
}
}
public ReporterNotice WrongCityNotice
{
get
{
return ReporterNotices.Where(n => n.Type == ReporterNoticeType.WrongCity).FirstOrDefault();
}
set { if(value==null)
{
ReporterNotices.Remove(ReporterNotices.Where(n => n.Type == ReporterNoticeType.WrongCity).First());
}
else
{
if (ReporterNotices.Where(n => n.Type == ReporterNoticeType.WrongCity).FirstOrDefault()==null)//there is always only max one instance of this type of notice
{
ReporterNotices.Add(value);
}
else
{
var c = ReporterNotices.Where(n => n.Type == ReporterNoticeType.WrongCity).First();
c = value;
}
}}
}
public VerificationViewModule()
{
ObservableCollection<ReporterNotice> loadedReporterNotices = new ObservableCollection<ReporterNotice>();
loadedReporterNotices.Add(new ReporterNotice() { Content = "Dublic", Type = ReporterNoticeType.WrongCity });
loadedReporterNotices.Add(new ReporterNotice() { Content = "Hilton", Type = ReporterNoticeType.Duplicate });
loadedReporterNotices.Add(new ReporterNotice() { Content = "Another notice", Type = ReporterNoticeType.Other });
ReporterNotices = loadedReporterNotices;
}
}
You can try the following. Implement the mediator to display windows and make sure that you use view models for the DataContext for both the main and edit windows. It is important to tell the main view model that the object is being deleted. This is done via a callback and routing that through a command on the EditNoticeViewModel
//This viewmodel is on the main windows datacontext
public class ParentViewModel
{
private readonly IWindowMediator _mediator;
public ParentViewModel(IWindowMediator mediator)
{
_mediator = mediator;
}
public ObservableCollection<Notice> Notices { get; private set; } //bound to list in xaml
public void OpenNotice(Notice notice)
{
//open the window using the Mediator pattern rather than a new window directly
_mediator.Open(new EditNoticeViewModel(notice, DeleteNotice));
}
private void DeleteNotice(Notice notice)
{
//This will remove it from the main window list
Notices.Remove(notice);
}
}
//view model for EditNoticeWindow
public class EditNoticeViewModel
{
public EditNoticeViewModel(Action<Notice> deleteCallback, Notice notice)
{
Model = notice;
DeleteCommand = new DelegateCommand((a) => deleteCallback(Model));
}
//Bind in xaml to the Command of a button
DelegateCommand DeleteCommand { get; private set; }
//bound to the controls in the xaml.
public Notice Model { get; private set; }
}
//This is a basic interface, you can elaborate as needed
//but it handles the opening of windows. Attach the view model
//to the data context of the window.
public interface IWindowMediator
{
void Open<T>(T viewModel);
}
Depending on implementation you might want to close the view when the delete button gets pushed. You can do this by implementing something like the as described here with respect to WorkspaceViewModel
Why don't you wrap the WrongCityNotice in a viewModel implementing IReporterNotice and having a reference to the parent viewmodel and a Delete method:
public void Delete() { _parentvm.Delete(_wrongCityNotice); }
You can use this wrapper as DataContext.
You're trying to destroy the DataContext. C# doesn't work that way. Setting an object reference to null doesn't delete the object, it only removes the reference to it. (When nothing references an object anymore it gets garbage collected, but you can't destroy an object directly).
DataContext = null only means that locally your DataContext doesn't point to any object any more. The main view model still has a reference however so nothing changes there. You'll have to ask the main view model to remove the notification from it's collection (probably through a callback method (Action) is best so you don't have to know about the parent view model).