I have got my MainWindow which loads new UserControls and there ViewModel's into it's ContentControl, so the Views are switched.
However, I need to access a property in my MainWindow ViewModel from a ViewModel within the ContentControl.
MainWindowViewModel
namespace PhotoManagement
{
public class MainWindowViewModel : NotifyUIBase
{
public ObservableCollection<ViewVM> Views { get; set; }
private ObservableCollection<Logged> loggedUsers;
public ObservableCollection<Logged> LoggedUsers
{
get
{
return loggedUsers;
}
set
{
loggedUsers.Add(value[0]);
//There is a user logged in, switch to home and display menu
if (loggedUsers.Count > 0)
{
//Display menu, switch Windows
MessageBox.Show("Someone is logged in!");
}
else
{
MessageBox.Show("No-one is logged in!");
}
}
}
Below you can see the LoginViewModel which is in the MainWindow ContentControl, I have added a comment where i'm trying to add this new user to the ObservableCollection.
#region Login Methods
private LoginVM loginVM;
public LoginVM LoginVM
{
get
{
return loginVM;
}
set
{
loginVM = value;
editEntity = editVM.TheEntity;
RaisePropertyChanged();
}
}
protected override void DoLogin()
{
//Check if email exists
var exist = db.Users.Count(a => a.Email == LoginVM.TheEntity.Email);
if (exist != 0)
{
//Fecth user details
var query = db.Users.First(a => a.Email == LoginVM.TheEntity.Email);
if (Common.Security.HashGenerator.CalculateHash(LoginVM.TheEntity.ClearPassword, query.Salt) == query.Hash)
{
//Password is correct
MessageBox.Show("Details correct!");
//Set properties
LoginVM.TheEntity.FirstName = query.FirstName;
LoginVM.TheEntity.LastName = query.LastName;
LoginVM.TheEntity.UID = query.UID;
//Add the LoginVM to LoggedUsers
Edit:
This is where I add the Views in MainWindowViewModel
namespace PhotoManagement
{
public class MainWindowViewModel : NotifyUIBase
{
public ObservableCollection<ViewVM> Views { get; set; }
private ObservableCollection<Logged> loggedUsers;
public ObservableCollection<Logged> LoggedUsers
{
get
{
return loggedUsers;
}
set
{
loggedUsers.Add(value[0]);
//There is a user logged in, switch to home and display menu
if (loggedUsers.Count > 0)
{
//Display menu, switch Windows
MessageBox.Show("Someone is logged in!");
}
else
{
MessageBox.Show("No-one is logged in!");
}
}
}
public string Version
{
get { return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(); }
}
public MainWindowViewModel()
{
ObservableCollection<ViewVM> views = new ObservableCollection<ViewVM>
{
new ViewVM { IconGeometry=App.Current.Resources["home4"] as Geometry, ViewDisplay="Home", ViewType = typeof(LoginView), ViewModelType = typeof(LoginViewModel)},
new ViewVM { IconGeometry=App.Current.Resources["instagram3"] as Geometry, ViewDisplay="Images", ViewType = typeof(LoginView), ViewModelType = typeof(LoginView)},
new ViewVM { IconGeometry=App.Current.Resources["money674"] as Geometry, ViewDisplay="Sales", ViewType = typeof(LoginView), ViewModelType = typeof(LoginViewModel)},
new ViewVM { IconGeometry=App.Current.Resources["printing1"] as Geometry, ViewDisplay="Print Queue", ViewType = typeof(LoginView), ViewModelType = typeof(LoginViewModel)},
new ViewVM { IconGeometry=App.Current.Resources["cog2"] as Geometry, ViewDisplay="Settings", ViewType = typeof(IconLibaryView), ViewModelType = typeof(IconLibaryViewModel)},
new ViewVM { IconGeometry=App.Current.Resources["upload40"] as Geometry, ViewDisplay="Upload", ViewType = typeof(IconLibaryView), ViewModelType = typeof(IconLibaryViewModel)}
};
Views = views;
RaisePropertyChanged("Views");
views[0].NavigateExecute();
}
}
}
You simply need to use Ancestor binding from within ContentControl's any child element :
{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}},Path=DataContext.AnyPropertyOfMainWindowViewModel}
If Window has MainWindowViewModel as DataContext.
I would go with events for ViewModel-to-ViewModel communication, and I prefer the IEventAggregator, available as a PubSub nuget package from Microsoft, but there are plenty to choose from (or roll your own if you prefer).
public MainViewModel() {
Aggregator.GetEvent<UserLoggedInEvent>().Subscribe(user => ...do your magic);
}
And in your LoginViewModel, publish it after the user has logged in:
public DoLogin() {
... do other stuff here...
Aggregator.GetEvent<UserLoggedInEvent>().Publish(userDetails);
}
Using the IEventAggregator from Prism, the event class is simple:
public class UserLoggedInEvent : PubSubEvent<User> {}
Btw - One of the main purposes for MVVM or any design pattern is to abstract UI from business code, so if you can remove all your App.Current.Resources stuff from your VM using a converter or something else then you've abstracted it from WPF (much more easily ported to other platforms like UWP).
Related
I am learning the whole new Universal Apps creation together with Prism and Unity, but I got a few questions I am not sure about:
I have the following simple data object:
public class Customer : IEditableObject, IEquatable<Customer>
{
private Customer backup;
public string Name { get; set; }
public string Surname { get; set; }
public DateTime DateOfBirth { get; set; }
public void BeginEdit()
{
this.backup = this.MemberwiseClone() as Customer;
}
public void CancelEdit()
{
this.Name = this.backup.Name;
this.Surname = this.backup.Surname;
this.DateOfBirth = this.backup.DateOfBirth;
}
public void EndEdit()
{
this.backup = this.MemberwiseClone() as Customer;
}
public bool WasChangeMade()
{
if (this.Equals(backup))
return false;
else
return true;
}
public bool Equals(Customer other)
{
return this.Name == other.Name &&
this.Surname == other.Surname &&
this.DateOfBirth == other.DateOfBirth;
}
}
Under my Main Page I have a simple ListBox, where I show collection of these Customers. Everything good so far.
Afterwards, when under my ListBox user selects any one of these Customer, then he can click Edit Settings button and edit properties of this selected Customer. It is a simple command:
cmd_EditCustomer = new DelegateCommand(() =>
{
_navigationService.Navigate(App.Experiences.Detail.ToString(), SelectedCustomer);
});
Which simply navigates to a new page (detail page, where user can do the changes) and the argument I pass here is the Selected Customer.
My DetailPage View Model looks like following:
public class DetailPageViewModel : ViewModel, Interfaces.IDetailPageViewModel
{
public DelegateCommand cmd_SaveChanges { get; set; }
public Customer SelectedCustomer { get; set; }
private readonly INavigationService _navigationService;
private readonly IDialogService _dialogService;
public DetailPageViewModel(INavigationService navigationService,
IDialogService dialogService)
{
_navigationService = navigationService;
_dialogService = dialogService;
InitializeCommands();
}
public override void OnNavigatedTo(object navigationParameter, NavigationMode navigationMode, Dictionary<string, object> viewModelState)
{
this.SelectedCustomer = navigationParameter as Customer;
this.SelectedCustomer?.BeginEdit();
}
private void InitializeCommands()
{
cmd_SaveChanges = new DelegateCommand(() =>
{
SelectedCustomer?.EndEdit();
_dialogService.Show("Changes Saved!");
_navigationService.Navigate(App.Experiences.Main.ToString(), null);
});
}
}
As you can see, this is a very simple application, which I only use for learning purposes. Here are my questions:
1) Is it good to pass Selected Customer in such a way as I did? (in the parameter of the INavigationService), or should I implement other logic?
2) When user makes a change to the Selected Customer and clicks Save Changes (the only command you can see there), it does not update the original Customer (from my original collection). How is this possible? How to achieve, that my Customer will be updated? Should I create PubSubEvent for this?
EDIT:
I have managed to locate the error - when user navigates back to MainPage, my MainPageViewModel is re-initializes, which re-populates collection of items. The question now is - how can I keep MainWindowViewModel alive thorough the applications life?
Re-populates collection of items from what?
You just need to save a new values, for example if you populate your customers from DB you have to call DB and save changes before navigate back etc, so after that when MainPageViewModel would be re-initializes you'll get your changes and changes performed by another users.
In the end, I found out that this was not a good way how to hold data in your application.
Based on what I have read, I should have implemented Repository Strategy, which is only referenced in a ViewModel such as:
public MainPageViewModel(IDataRepository dataRepository, INavigationService navService, ...){etc.}
Example of a simplified interface:
public interface IDataRepository
{
List<string> GetListOfStrings();
string GetUserEnteredData();
void SetUserEnteredData(string data);
}
This is how you initialize it in UnityContainer:
_container.RegisterType<IDataRepository, DataRepository>();
You can read more from Patterns & Practices team in here:
https://prismwindowsruntime.codeplex.com/
I have a problem using the IDataErrorInfo in combination with IReactiveBinding.Bind(). I hope someone here can help me.
I have a ViewModel that is inherited from ReactiveObject and implements the IDataErrorInfo interface.
public class MainWindowViewModel : ReactiveUI.ReactiveObject, IDataErrorInfo
{
private string username = string.Empty;
public string Username
{
get { return this.username; }
set { this.RaiseAndSetIfChanged(ref this.username, value); }
}
public MainWindowViewModel()
{
this.Validator = new MainWindowViewModelValidator();
}
public AbstractValidator<MainWindowViewModel> Validator { get; set; }
#region IDataErrorInfo Members
string IDataErrorInfo.Error
{
get
{
return Validator != null ? string.Join(Environment.NewLine, Validator.Validate(this).Errors.Select(x => x.ErrorMessage).ToArray())
: string.Empty;
}
}
string IDataErrorInfo.this[string propertyName]
{
get
{
if (Validator != null)
{
var results = Validator.Validate(this, propertyName);
if (results != null
&& results.Errors.Count() > 0)
{
var errors = string.Join(Environment.NewLine, results.Errors.Select(x => x.ErrorMessage).ToArray());
return errors;
}
}
return string.Empty;
}
}
#endregion
}
The MainWindowViewValidator ensures that the Usernameproperty is not empty.
The ViewModel is connected to the View in the code behind of the XAML-File:
public partial class MainWindow : IViewFor<MainWindowViewModel>
{
public MainWindow()
{
InitializeComponent();
this.ViewModel = new MainWindowViewModel();
this.Bind(this.ViewModel, viewmodel => viewmodel.Username, view => view.Username.Text);
}
public MainWindowViewModel ViewModel
{
get { return (MainWindowViewModel)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register("ViewModel", typeof(MainWindowViewModel), typeof(MainWindow), new PropertyMetadata(null));
object IViewFor.ViewModel
{
get { return ViewModel; }
set { ViewModel = (MainWindowViewModel)value; }
}
}
The problem is now that the model validation is not called, as I don't specify the databinding in the XAML file directly.
Does anybody has a neat solution for this problem?
The problem is now that the model validation is not called, as I don't specify the databinding in the XAML file directly.
ReactiveUI doesn't participate in IDataErrorInfo and friends for binding. It used to, but really if you think about it, Validation itself is a derived property of the form.
ReactiveUI is already really good at describing how properties are related to each other (via WhenAny/ToProperty), so you should just construct an ValidationError property that displays the error message.
I have a ViewModel called MainViewModel (of course) which contains multiple Contructors as per the below:
[ImportingConstructor]
public MainViewModel(IWindowManager windowManager)
: this(windowManager, new DataProvider(), new LocalJsonPersistenceManager())
{
}
[PreferredConstructorAttribute]
public MainViewModel(IWindowManager windowManager, IInformationProvider infoProvider,
IPersistenceManager persistenceManager)
{
//generating data, handling clicks etc.
}
Inside that ViewModel is a public item that is constantly being updated (whenever a user clicks on a certain button and takes some actions on the form):
public Item ClickedItem
{
get { return clickedItem; }
set
{
clickedItem = value;
NotifyOfPropertyChange(() => ClickedItem);
if (ClickedItem != null)
{
FindNextItem();
}
}
}
Now i have a UserControl I am building that contains a ListView that I personnalised to make it a sticky headered listview (header moves up whenever the next header is reached blabla ...). because I can only do this via a GroupStyled ListView, I must build the data for the ListView in the C# code behind.
EDIT:
I am trying it using a ViewModelLocator as such:
public class ViewModelLocator
{
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
if (ViewModelBase.IsInDesignModeStatic)
{
// Create design time view services and models
//SimpleIoc.Default.Register<IDataService, DesignDataService>();
}
else
{
// Create run time view services and models
//SimpleIoc.Default.Register<IDataService, DataService>();
}
SimpleIoc.Default.Register<MainViewModel>();
}
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
public static void Cleanup()
{
// TODO Clear the ViewModels
}
}
And I am calling up the data's specific value as such:
var vm1 = (new ViewModelLocator()).Main;
testtxt.Text = vm1.ClickedItem.Name;
But it keeps giving me an error message on runtime on the line:
return ServiceLocator.Current.GetInstance<MainViewModel>();
in the ViewModelLocator's block:
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
With the error message {"Type not found in cache: Caliburn.Micro.IWindowManager."} and an InnerException message of null.
It looks like MEF cannot construct the IWindowManager, which is a dependency of your ViewModel.
Try registering at least the default instance from Caliburn.
Update
taken straight from caliburn.Micro's MEF-Bootstrapper:
container = CompositionHost.Initialize(
new AggregateCatalog(
AssemblySource.Instance.Select(x => new AssemblyCatalog(x)).OfType<ComposablePartCatalog>()
)
);
var batch = new CompositionBatch();
batch.AddExportedValue<IWindowManager>(new WindowManager());
batch.AddExportedValue<IEventAggregator>(new EventAggregator());
batch.AddExportedValue(container);
container.Compose(batch);
You can check the sample code from Caliburn.Micro Github Samples.
https://github.com/Caliburn-Micro/Caliburn.Micro/blob/master/samples/Caliburn.Micro.HelloMef/Caliburn.Micro.HelloMef/MefBootstrapper.cs
You need to register your WindowManager in MEF.
I'm developing simple application in WPF with MVVM Light Toolkit. I have two views:
HomeView (default)
CustomersView
This is part of the MainViewModel class:
public MainViewModel()
{
CurrentViewModel = Bootstrapper.Instance.Container.Resolve<HomeViewModel>();
}
private void ExecuteShowCustomersCommand()
{
CurrentViewModel = Bootstrapper.Instance.Container.Resolve<CustomersViewModel>();
}
In CustomerViewModel I have property:
public ObservableCollection<Customers> Customers
{
get { return _customers; }
set
{
if (_customers == value) return;
_customers = value;
RaisePropertyChanged(CustomersPropertyName);
}
}
And my question is, when I should call the web service to get customers data? In CustomerViewModel constructor?
I would do it in the constructor of the viewmodel and use a IoC Container to get the instance.
Application start
SimpleIoc.Default.Register<IDataService, DataService>();
SimpleIoc.Default.Register<MyViewModel>();
ViewModel
public MyViewModel(IDataService DataService)
{
Mydata = DataService.GetData(); // Edit: Could also be done in a property with lazy load
}
Locator
public MyViewModel MyVM
{
get
{
return SimpleIoc.Default.GetInstance<MyViewModel>();
}
}
I have a simple sample that my sample has 2 window :
1-ProductlistView 2-ProductEditView (1-ProductlistViewModel 2-ProductEditViewModel)
I want the user can select a product in my ProductlistView and edit selected product in ProductEditView ...i'm using from this code in my sample:
public Class ProductEditViewModel:ViewModelBase
{
private readonly ProductEditView View;
public ProductModel Model { get; set; }
public ProductEditViewModel(Product myproduct)
{
View = new ProductEditView { DataContext = this };
if(myproduct!= null) Model = myproduct;
}
private bool IsInDialogMode;
public bool? ShowDialog()
{
if (IsInDialogMode) return null;
IsInDialogMode = true;
return View.ShowDialog();
}
}
and write to my editCommant in ProductlistViewModel:
private RelayCommand UpdateProductmdInstance;
public RelayCommand UpdateProductCommand
{
get
{
if (UpdateProductmdInstance!= null) return UpdateProductmdInstance;
UpdateProductmdInstance= new RelayCommand(a => OpenProductDetail(SelectedProduct), p => SelectedProduct!= null);
return UpdateProductmdInstance;
}
}
private void OpenProductDetail(Product product)
{
var ProductEditViewModel= new ProductEditViewModel(product);
var result = personDetailViewModel.ShowDialog();
...
}
I was wondering my sample is wrong?
Can i have an instance from a view in its viewmodel?
If my Sample is wrong how can i do this solution(send an object to other window and after edit get it)?
It is normally recommended to NOT have a ViewModel referencing a View. See this question on how to show a dialog from ViewModel.