In Xamarin for mac, I decided to make multiple views to be used within my main view using the MVVM pattern.
The thing is that I have a ListView within my MainPage which pulls a List of items from a model, and the list is populated within a child view, with its own ViewModel.
When I add a new service from the child view, I would like for the OnPropertyChanged event on the parent view model to trigger.
It is working by navigating to the parent view and setting the animation to false, but this is not really nice looking. It worked though when I had all code within one ViewModel.
How I tried to achieve this, and the errors I got:
0 - Accessing the command within the child model from the parent model, and passing the propertychanged event handler along.
I Couldn't do it. I tried this by making a bindable command like below, but this is not doable for me as I don't think it is possible for the command to know when the property will be changed, which is the whole point of this problem.
If it is doable, I don't know how.
//public static readonly BindableProperty SaveServiceClickedCommandProperty =
// BindableProperty.Create(
// "SaveServiceClicked",
// typeof(Command),
// typeof(NewServiceViewModel),
// null);
1 - Passing the parent view model on the child view model, and put a OnPropertyChanged(nameof(parentModel.List)) at the clicked event handler.
public class ChildViewModel : INotifyPropertyChanged
{
public ICommand AddEntryClickedCommand { get; private set; }
private MainModel mainModel;
// property changed handler
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public NewServiceViewModel()
{
Navigation = MainPage;
//async void execute() => await OpenPage();
//OpenPageCommand = new Command(execute, () => !IsBusy);
//async Task OpenPage()
//{
// await Navigation.PushAsync(new MainPage());
//}
// Here I tried to access the data from within the main model.
mainModel = new MainModel(Navigation);
InitMainModel();
void InitMainModel()
{
MainPage mainView = new MainPage();
mainView.BindingContext = mainModel;
}
async void c1() => await AddEntryClicked();
AddEntryClickedCommand = new Command(c1);
}
public async Task<bool> AddEntryClicked()
{
OnPropertyChanged(nameof(mainModel.List))
}
The attempt above created some errors as the object is already populated.
Leading to me thinking that I don't have the right approach altogether.
My solution being to re-introduce the child view within the parent view, and change IsVisible according to the button being clicked or not, as I already did with other smaller component.
I have thought about pulling the list from the child view, but that's raises the same issue of non-null collection.
Of course, the code has been modified to show only the gist.
Thanks in advance.
Related
My aim is to access bindable property across the the App. But My current framework ViewModel Instance create multiple time
My Requirement : I have the cart count in the bottomTray(CheckuoutViewModel) i want to increase the cart count any where in the app page, but in this cart count not update when back click, its only working on forward navigation, the reason behind CheckoutViewModel instance create each and every time. so that i'm try to instant creation at earlier.
Here I'm list out sample ViewModel and calling method
Login ViewModel
Checkuout ViewModel(This view model common for all page)
BaseNavigationViewModel(Its BaseViewModel)
As of now i'm calling when BindinContext each and every time like,
new LoginViewMode(navigation)
new CheckoutViewModel(navigation)
what will do to create all ViewModel instance when app start time like ViewModel Locator?
Im tried
public static ViewModelLocator Locator
{
get { return locator ?? (locator = new ViewModelLocator()); }
}
And ViewModel Locator
public ViewModelLocator()
{
navigation = App.Current.MainPage.Navigation;
}
internal CustomTabBarViewModel CustomTabBarVM
{
get
{
return customTabBarVM ?? (customTabBarVM = new CustomTabBarViewModel(navigation));
}
}
And CustomTabBar.xaml.cs
public CustomTabBar()
{
viewModel = App.Locator.CustomTabBarVM;
InitializeComponent();
BindingContext = viewModel;
}
and Expectation
App.Locator.CustomTabBarVM.BadgeCartCount = OrderObject.Instance.ORDER_OBJECT.Items.Count;
This approach is working fine but it's create some navigation issues
A singleton instance is a common feature of virtually all MVVM frameworks (Prism, FreshMVVM etc). If you aren't using a framework (if you aren't, I would STRONGLY advise you consider using one), below is a solution.
To obtain a single instance of a ViewModel you can use the App class to host the object and access it whenever you need.
Create a public static property of your ViewModel:
public static MyViewModel MyViewModelInstance { get; }
Create an instance in the constructor of the app
public App()
{
InitializeComponent();
MyViewModelInstance = new MyViewModel();
var myPage = new MyPage()
{
BindingContext = MyViewModelInstance
};
var navPage = new NavigationPage(myPage);
MainPage = navPage;
}
Whenever you create a new page, access the shared instance
// This method is just an example of how you might create a new page and wire up the view model
async void GoNextClicked(System.Object sender, System.EventArgs e)
{
var myPage = new MyPage()
{
BindingContext = App.MyViewModelInstance
};
await this.Navigation.PushAsync(myPage);
}
This approach comes with a few caveats, you are creating instances when the app loads not when they are needed (Eagerly loading). So a performance optimisation would be to use Lazy<T> to handle the creation of these objects. However this is logic that has already been written for you in MVVM frameworks, they are there to help you and you should be using them.
Lazy Load
You can save memory and performance at startup by lazy loading the viewmodel, here is this example rewritten to support this pattern:
public static MyViewModel MyViewModelInstance
{
get => _myViewModelInstanceFactory.Value;
}
private static Lazy<MyViewModel> _myViewModelInstanceFactory = new Lazy<MyViewModel>(() => new MyViewModel(), true);
public App()
{
InitializeComponent();
var myPage = new MyPage()
{
BindingContext = MyViewModelInstance
};
var navPage = new NavigationPage(myPage);
MainPage = navPage;
}
Now this object won't be created until it is accessed by your code, and once it has been accessed once it has already been created and will go on to live in memory for the rest of your apps lifecycle.
Axemasta has good answer about re-use of a shared view model instance.
I'll give an alternative approach to the underlying need given in one comment: how to have a static property (so the value is common), and Bind to it when Binding to an instance.
Use this approach if you do want a different CheckoutViewModel for each new page. For example, if there are other properties that should be set up differently, depending on the page.
public class CheckoutViewModel : : INotifyPropertyChanged // or your MVVM library's base class for ViewModels.
{
public static int SharedCount { get; set; }
public void IncrementCount()
{
Count = Count + 1;
}
public int Count {
get => SharedCount;
set {
// Exact code might be simpler if using an MVVM library.
if (SharedCount != value)
{
SharedCount = value;
OnPropertyChanged("Count");
}
}
}
}
}
LIMITATION: This assumes that only the current instance of CheckoutViewModel is visible; if you need to "notify" OTHER Views (update other CheckoutViewModel instances), then you'll need a "publish/subscribe" solution. In Xamarin Forms, one such solution is MessagingCenter.
I'm using MVVMCross 7.1.2 and have a situation where several of my pages can't inherit the MvxContentPage class. Understandably this breaks a few things that MVVMCross implements.
One thing I noticed is the BindingContext for the page does not get set and as a result we get a NullReference exception which is difficult to debug.
What is the best was to access the ViewModel Instance form the Views code behind ? At the moment I'm using the interface IMvxOverridePresentationAttribute and then implementing it like this:
public MvxBasePresentationAttribute PresentationAttribute(MvxViewModelRequest request)
{
BindingContext = ((MvxViewModelInstanceRequest) request).ViewModelInstance;
InitializeComponent(); <--- Update 1,moved from ctor
return null;
}
Is this the best way to get the VM instance ? or is there a better to the BindingContext automatically set.
UPDATE 1;
I still get the NullReference Exception with this method presumably as it sets the BindingContext after InitializeComponent is called. I've tried moving the InitializeComponent call to after the BindingContext is set but the page doesn't render correctly.
Have you tried something like this?
public partial class SomeView : ContenPage
{
public ViewModels.NestedViewModel ViewModel {get; set;}
public SomeView()
{
InitializeComponent();
// when viewmodel already created
if (Mvx.IoCProvider.TryResolve<ViewModels.NestedViewModel>(out var someViewModel))
{
ViewModel = someViewModel;
BindingContext = ViewModel;
return;
}
// creating viewmodel
var _viewModelLoader = Mvx.IoCProvider.Resolve<IMvxViewModelLoader>();
var request = new MvxViewModelInstanceRequest(typeof(ViewModels.NestedViewModel));
request.ViewModelInstance = _viewModelLoader.LoadViewModel(request, null);
ViewModel = request.ViewModelInstance as ViewModels.NestedViewModel;
BindingContext = ViewModel;
Mvx.IoCProvider.RegisterSingleton<ViewModels.NestedViewModel>(ViewModel);
}
}
I'm trying to make use of the new SearchHandler implemented as part of Xamarin Forms 4. I've found it pretty easy so far to get suggestions populated but now I want to raise an event, or follow the suggested method of handling when a search is confirmed.
public class FoodSearchHandler: SearchHandler
{
IFoodDataStore dataStore = new FoodDataStore();
protected override void OnQueryConfirmed()
{
base.OnQueryConfirmed();
// What to do here?
}
protected override void OnQueryChanged(string oldValue, string newValue)
{
base.OnQueryChanged(oldValue, newValue);
if(!string.IsNullOrWhiteSpace(newValue)
{
// Populate suggestions
ItemsSource = dataStore.GetSuggestions(newValue);
}
else
{
ItemsSource = null;
}
}
}
public partial class FoodsPage : ContentPage
{
ObservableCollection<Food> Foods = new ObservableCollection<Food>();
public ItemsPage()
{
InitializeComponent();
// Wire up the search handler
Shell.SetSearchHandler(this, new FoodSearchHandler());
BindingContext = this;
}
}
Unfortunately, althought the alpha docs mention the search handler they don't contain any details on how to use it and the sample apps only demonstrate populating the suggestions.
Does anyone out there have a pointer to offer on how I should be notifying my ContentPage that my SearchHandler confirmed a search?
So, after reading the Shell docs some more, it seems what I want to do in this situation is use of Shell's new Navigation and navigate to a route passing the search text as a query, for example:
protected override void OnQueryConfirmed()
{
base.OnQueryConfirmed();
var shell = Application.Current.MainPage as Shell;
shell.GoToAsync($"app:///fructika/search?query={Query}", true);
}
N.B. It doesn't look like passing data works right now or if it does I'm doing it wrong but I'll raise a separate question about that.
I have a view which consists of a button when i click i want to open window in which i want to pass observable collection to viewmodel of the new window open.I am using below code it is working but i am not sure it is mvvm pattern or not.
ViewCode:
NewWindow newWindowDialog;
newWindowDialog = new NewWindow()
{
newWindowDialogCollection = suppliersList,
Owner = Application.Current.MainWindow
};
newWindowDialog.ShowDialog();
NewWindow Dialog Code:
public partial class NewWindow : Window
{
public NewWindow()
{
InitializeComponent();
newWindowDialogCollection = new ObservableCollection<SModel>();
DataContext = this;
}
public ObservableCollection<ISupplierModel> newWindowDialogCollection { get; set; }
}
In xaml "newWindowDialogCollection" act as my data source for binding
But i am not sure above way is right way to impliment to open new window and pass collection. and i want to do it by pure mvvm & viewmodel.
Please let me know your thoughts
Creating a new window in a viewmodel couples the View and the ViewModel layers tightly, which defeats the purpose of MVVM.
Also, you shouldn't have business data in the View, but in a ViewModel instead, even with dialogs.
What you'll see some MVVM frameworks do is implementing service classes for everything that is not (easily) manageable by either the Model, the View or the ViewModel. Displaying dialogs is one of those things.
I'll use the example of Catel to demonstrate.
Catel offers a IUIVisualizerService interface, that you can inject in the constructor of your viewmodel:
public MyViewModel(IUIVisualizerService visualizerService)
{
this._visualizerService = visualizerService;
}
And to open a new dialog, since Catel internally matches views and viewmodels, you simple create the dialog's viewmodel and resolve the appropriate view. This way, you can pass you data to the viewmodel's constructor as you please:
var viewModel = new MyViewModel(suppliersList);
_visualizerService.Show(viewModel);
You could create a window service that is responsible for open the window and then inject your view model with such a service. You then call the service's ShowWindow method to open the window from your view model, e.g.:
Service:
public interface IWindowService
{
void ShowWindow(ObservableCollection<string> collection);
}
public class WindowService : IWindowService
{
public void ShowWindow(ObservableCollection<string> collection);
{
NewWindow newWindowDialog = new NewWindow()
{
newWindowDialogCollection = collection,
Owner = Application.Current.MainWindow
};
newWindowDialog.ShowDialog();
}
}
View Model:
public class ViewModel
{
private readonly IWindowService _service;
public ViewModel(IWindowService service)
{
_service = service;
}
//...
public void OpenCommandExecuted()
{
_service.ShowWindow(_theCollectionToPass);
}
}
Using this approach the view model only knows about an interface that you can easily unit test, without actually opening up a window, by providing a mock implementation of the interface.
I have two VM - View (inherited from Screen) and Edit (inherited from Screen). View is used to display grid with data and Edit - add/edit new items into grid.
In my ShellViewModel I have the following code to activate View.
public void WorkstationView()
{
this.ActivateItem(ServiceLocator.Current.GetInstance<WorkstationViewModel>());
}
In WorkstationViewModel when user clicks on the Create button the following code is invoked
public void CreateAction()
{
EditableObject = new WorkstationDto();
TryClose(true);
}
And there is a listener to Deactivated event property, see code below (InitViewModels is invoked in ShellViewModel constructor).
private void InitViewModels()
{
#region Init
WorkstationViewModel = ServiceLocator.Current.GetInstance<WorkstationViewModel>();
WorkstationEditViewModel = ServiceLocator.Current.GetInstance<WorkstationEditViewModel>();
#endregion
#region Logic
WorkstationViewModel.Deactivated += (o, args) =>
{
if (WorkstationViewModel.EditableObject == null)
{
return;
}
WorkstationEditViewModel.EditableObject = WorkstationViewModel.EditableObject;
ActivateItem(WorkstationEditViewModel);
};
#endregion
}
The problem here is a StackOverflow exception when I close Edit view (see create action).
“Since the Conductor does not maintain a “screen collection,” the activation of each new item causes both the deactivation and close of the previously active item.” Caliburn.Micro documentation
If you are using Conductor<T>, then ActivateItem(WorkstationEditViewModel); inside of the Deactivated handler is implicitly re-triggering the deactivation of the previous viewmodel - giving you an infinite loop. Try changing your conductor to inherit from Conductor<IScreen>.Collection.OneActive instead. However, you will still have two deactivations: the one from the original TryClose operation, and a second one when you activate the new screen. Overriding DetermineNextItemToActivate can help you avoid that.