Windows Phone Page navigation in MVVM - windows

I am using MVVM in one of the app. I have created different project for Model, View and View Model.
I need to navigate to another XAML from ViewModel. I found some solution using MVVM light. Is there any way of implementing navigation from view model without using MVVM light.

Simple as that,
IF you want to navigate from page1 to page2,
private void MoveToPage2FromPage1()
{
NavigationService.Navigate(new Uri("/Page2.xaml", UriKind.Relative));
}
How to perform page navigation on Windows Phone 8

You can store current page url on a notify property of Shared ViewModel in App. After that, it is easy to catch the change of this url and navigate to the correct url by observing it.
public class AppViewModel : INotifyPropertyChanged
{
public string CurrentPageURL { get; set; }
private string _currentPageURL;
public string CurrentPageURL
{
get { return _currentPageURL;}
set
{
if (_currentPageURL==value)
return; // to prevent reload the same page.
_currentPageURL = value;
NotifyPropertyChangedCurrentPageURL
}
}
// INotifyPropertyChanged implementations
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
// Store in static singleton instance of AppViewModel
public class App : Application
{
private static Lazy<AppViewModel> _ViewModel=new Lazy<ViewModel>();
public static AppViewModel ViewModel { get { return _ViewModel.Value; } }
....
public App()
{
AppViewModel.PropertyChanged=(s,a) =>
{
if (a.PropertyName=="CurrentPageURL")
{
NavigationService.Navigate(new Uri(AppViewModel.CurrentPageURL, UriKind.Relative));
};
}
}
}
// Usage sample
public class Page1ViewModel
{
private btnMoveNextPage_Click(object s, EventHandler a) {
App.ViewModel.CurrentURL="~/Page2.xaml";
}
}

Related

Getting initial data pattern in Xamarin Forms

I'm trying to understand the pattern to use in Xamarin Forms when a page gets its initial data from a web API.
The page is tied to a ViewModel. Let's use this simple example:
public class DataFeedViewModel : BaseViewModel
{
public DateFeedViewModel()
{
Title = "My Feed";
}
public List<FeedItem> Feed { get; set; }
}
The DataFeedViewModel is bound to the page:
public MainPage()
{
InitializeComponent();
this.BindingContext = new DataFeedViewModel();
}
As I understand it, I use the OnAppearing() method to fetch my initial set of data from the backend API:
protected override async void OnAppearing()
{
base.OnAppearing();
var result = await _myApiService.GetFeed();
// What's next? Do I simply do the following?
// new DataFeedViewModel
// {
// Feed = result
// }
}
Also a second but very important question is whether this pattern is the recommended approach.
As I learn about Xamarin and .NET Maui, I understand, the trend is to go from an event driven model to a more MVVM command driven approach.
I'm a bit confused about how to use a ViewModel to tap into these life cycle methods such as OnAppearing().
create an Init method on your VM
public class DataFeedViewModel : BaseViewModel
{
public DateFeedViewModel()
{
Title = "My Feed";
}
public List<FeedItem> Feed { get; set; }
public async void Init()
{
Feed = await _myApiService.GetFeed();
}
}
and then have your page call it
private DataFeedViewModel VM { get; set; }
public MainPage()
{
InitializeComponent();
this.BindingContext = VM = new DataFeedViewModel();
}
protected override async void OnAppearing()
{
base.OnAppearing();
await VM.Init();
}

ICommand Xamarin Forms

I have a strange issue of getting null exception. I created class that implements ICommand interface, I have two methods.
public void Execute(object parameter)
{
NavigateAsync();
}
private async void NavigateAsync()
{
await App.MainNavigation.PushAsync(new Pages.SettingsPage());
}
When NavigateAsync() is exectude my MainNavigation is always null, even that I can see that parameter inside Execute is set.
In my App.xaml.cs file I have created public static INavigation MainNavigation { get; set; }
public partial class App : Application
{
public static ViewModels.MainViewModel ViewModel { get; set; }
public static INavigation MainNavigation { get; set; }
public App ()
{
InitializeComponent();
MainPage = new NavigationPage(new Paperboy.MainPage());
}
protected override void OnStart ()
{
// Handle when your app starts
}
protected override void OnSleep ()
{
// Handle when your app sleeps
}
protected override void OnResume ()
{
// Handle when your app resumes
}
}
Se when clicking icon i can se that command is executed but App.MainNavigation inside NavigateAsync() in null. So command is not executing PushAsync to SettingsPage.
You never instantiate your static MainNavigation property...
If I can give you 2 remarks:
instead of using a static property declared into your App.xaml.cs, maybe a better implementation could be to embed a Navigation getter into a specific 'service' class or directly into your Command definition:
public class MyCommand : ICommand
{
...
// Navigation getter
// There are better places for this prop but it's better
// than in app.xaml.cs
private INavigation MainNavigation
{
get => Application.Current?.MainPage?.Navigation;
}
public void Execute(object parameter)
{
NavigateAsync();
}
private async void NavigateAsync()
{
try
{
await MainNavigation?.PushAsync(new Pages.SettingsPage());
}
catch(){ ... }
}
}
Another point I would like to note, is that your Command looks like asynchronous. Maybe you already know, but here is a good implementation of async commands to avoid app crashes: Asynchronous commands
I hope it can help you. Happy coding !

Scrolling to start of Xamarin Forms ListView with header

I'm having some trouble scrolling to the top of a ListView in Xamarin Forms. I can scroll to the first item by calling ScrollTo and passing the first item. The problem is that when the list has a header item, I can't find a way to scroll to the header. Is this possible? The only work around I can think of is to not use the header and just have another item at the start of the ItemSource list that acts as a header but I'd rather use the header if possible. Thanks.
So I've solved this myself now. My solution was to subclass ListView and add a public ScrollToTop method which invokes an internal ScrollToTopRequestedEvent when called. I then subclassed the ListViewRenderer on each platform and registered for the event.
In the Android renderer I'm then calling Control.SmoothScrollToPositionFromTop(0, 0) to scroll to top.
In the iOS rendered I'm calling Control.ScrollRectToVisible(new CoreGraphics.CGRect(0, 0, 1, 1), true).
Wah all credits #Gareth Wynn, man that was cool thx.
Anyway here's the code for everyone to use, change class names and namespace, iOS not included, do same as for Android just using Gareth Wynn's hint in parallel answer:
SHARED NiftyListView.cs :
using System;
using Xamarin.Forms;
namespace AppoMobi
{
public class NiftyListView : CListView
{
public event EventHandler EventScrollToTop;
//-------------------------------------------------------------------
public void ScrollToTop(bool animate=true)
//-------------------------------------------------------------------
{
//bool animate is not used at this stage, it's always animated.
EventScrollToTop?.Invoke(this, EventArgs.Empty);
}
}
}
ANDROID NiftyListView.Android.cs :
using System;
using AppoMobi;
using AppoMobi.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using ListView = Xamarin.Forms.ListView;
[assembly: ExportRenderer(typeof(NiftyListView), typeof(NiftyListViewRenderer))]
namespace AppoMobi.Droid.Renderers
{
//-------------------------------------------------------------------
class NiftyListViewRenderer : ListViewRenderer
//-------------------------------------------------------------------
{
protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
{
base.OnElementChanged(e);
var view = (NiftyListView)Element;
if (view == null) return;
view.EventScrollToTop += OnScrollToTop;
}
//-------------------------------------------------------------------------------------------
public async void OnScrollToTop(object sender, EventArgs eventArgs)
//-------------------------------------------------------------------------------------------
{
Control.SmoothScrollToPositionFromTop(0, 0);
}
}
}
ScrollTo(Object, Object, ScrollToPosition, Boolean)
Scrolls the ListView to the item in the group
https://learn.microsoft.com/en-us/dotnet/api/xamarin.forms.listview.scrollto?view=xamarin-forms
Model for each group:
public class Notification {
public int Id { get; set; }
public string Title { get; set; };
public Notification(int id, string title) {
Id = id;
Title = title;
}
}
Group-Model for ItemSource:
public class NotificationGroup: List<Notification> {
public string Title { get; set; }
public string ShortTitle { get; set; }
public NotificationGroup(string title) {
Title = title;
}
}
Sample Data & Usage:
//SAMPLE DATA
var notifications = new ObservableCollection <NotificationGroup> {
new NotificationGroup("Group-01") {
new Notification(1, "Item-1"),
new Notification(2, "Item-2")
},
new NotificationGroup("Group-02") {
new Notification(3, "Item-3"),
new Notification(4, "Item-4")
}
};
YourListViewName.ItemSource = notifications;
//USING
YourListViewName.ScrollTo(notifications.First()[0], notifications.First(), ScrollToPosition.Start, true);

Custom cell is not updating viewmodel when user enters input into cell

I am new into MVVMCross(Xamarin.iOS). So, I could be in wrong direction. If somebody can point me in right direction or point out what I am doing wrong.
I have already taken a look over "CustomerManagement" and "Collection" sample of MVVMCross.
Basically I am trying to create Settings screen.
I have one custom UITableViewCell with one text field. See my code below. "EntryTF" is IBOutleted. "EntryTF" is binded with "FirstName" property of Model. Setting value to "FirstName" is reflecting on UI. But if user enter something to textfield doesn't save to model. In short cell is not updating viewmodel/model.
Please note that I want to keep binding out of cell class. So, I can reuse this cell for other models or fields.
public partial class PlainEntryCell : UITableViewCell
{
public static readonly UINib Nib = UINib.FromName ("PlainEntryCell", NSBundle.MainBundle);
public static readonly NSString Key = new NSString ("PlainEntryCell");
public PlainEntryCell (IntPtr handle) : base (handle)
{
// this.DelayBind (() => {
// this.AddBindings(new Dictionary<object,string> ())
// });
}
public static PlainEntryCell Create ()
{
return (PlainEntryCell)Nib.Instantiate (null, null) [0];
}
public string CaptionText {
get {
return EntryTF.Text;
}
set {
EntryTF.Text = value;
}
}
}
My View Model:
public class RegisterViewModel: MvxViewModel
{
private RegisterModel _registerModel;
public RegisterViewModel ()
{
_registerModel = new RegisterModel ();
_registerModel.FirstName = "Test";
}
public RegisterModel Customer {
get { return _registerModel; }
set {
_registerModel = value;
RaisePropertyChanged ("Customer");
}
}
}
Model:
public class RegisterModel:MvxNotifyPropertyChanged
{
private string _firstName;
public string FirstName {
get { return _firstName; }
set {
_firstName = value;
RaisePropertyChanged ("FirstName");
}
}
public string LastName { get; set; }
public string PhoneNum { get; set; }
public string Email { get; set; }
public string Pin { get; set; }
}
TableView Source:
public class RegisterTableViewSource: MvxTableViewSource
{
RegisterView _registerView;
public RegisterTableViewSource (UITableView tableView, RegisterView registerView)
: base (tableView)
{
_registerView = registerView;
tableView.RegisterNibForCellReuse (PlainEntryCell.Nib,
PlainEntryCell.Key);
//tableView.RegisterNibForCellReuse (UINib.FromName ("DogCell", NSBundle.MainBundle), DogCellIdentifier);
}
protected override UITableViewCell GetOrCreateCellFor (UITableView tableView, Foundation.NSIndexPath indexPath, object item)
{
var cell = TableView.DequeueReusableCell (PlainEntryCell.Key, indexPath);
cell.Bind (_registerView, "CaptionText Customer.FirstName");
return cell;
//return (UITableViewCell)TableView.DequeueReusableCell (PlainEntryCell.Key, indexPath);
}
public override nint RowsInSection (UITableView tableview, nint section)
{
return 2;
}
}
Update:
Still not able to get the answer.
In above code I want to bind "EntryTF" with property of model. But I want to keep binding outside class. So, CaptionText property is not necessary if someone can point out direct way of binding to "EntryTF"
Is not there a way of creating BindableProperty just like Xamarin Forms? I feel MVVMCross is matured framework so, why there is not a solution of this kind of simple things.
I would also love to here if there is any simple/other way of achieving the same things.
I have also looked MTD but didn't find much useful for custom cell and it is also need good amount of my learning.
Basically I am trying to create Settings screen.
Take a look at using monotouch dialog, mt.d, with MvvmCross, Use Mvvmcross Binding with MonoTouch.Dialog (Lists and Commands)
That StackOverflow question/answer will get you started using mt.d with mvvmcross. I use it for basic settings screens in my applications.
Cell Isn't Updating
I wrote an article on using custom cells with MvvmCross, see
http://benjaminhysell.com/archive/2014/04/mvvmcross-custom-mvxtableviewcell-without-a-nib-file/
I have found it easier not to use nib files and fully describe my UI in code.
It looks like in your public partial class PlainEntryCell : UITableViewCell you don't ever bind the cell to the ViewModel. You have that commented out. I would try something like, while adding http://slodge.blogspot.com/2013/07/playing-with-constraints.html to your application for layout:
public PlainEntryCell()
{
CreateLayout();
InitializeBindings();
}
UILabel captionText;
private void CreateLayout()
{
SelectionStyle = UITableViewCellSelectionStyle.None;
Accessory = UITableViewCellAccessory.DisclosureIndicator;
captionText = new UILabel();
ContentView.AddSubviews(captionText);
ContentView.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
const int vPadding = 10;
const int hPadding = 20;
ContentView.AddConstraints(
captionText.AtTopOf(ContentView).Plus(vPadding),
captionText.AtLeftOf(ContentView).Plus(hPadding),
captionText.Width().EqualTo(UIScreen.MainScreen.Bounds.Width / 2)
);
private void InitializeBindings()
{
this.DelayBind(() =>
{
var set = this.CreateBindingSet<PlainEntryCell, RegisterViewModel();
set.Bind(captionText).To(vm => vm.CaptionText);
set.Apply();
});
}
}
Your custom cell needs to implement INotifyPropertyChanged to allow the ViewModel to be notified of a value change in your cell properties.
public partial class PlainEntryCell : UITableViewCell, INotifyPropertyChanged
{
public static readonly UINib Nib = UINib.FromName ("PlainEntryCell", NSBundle.MainBundle);
public static readonly NSString Key = new NSString ("PlainEntryCell");
public PlainEntryCell (IntPtr handle) : base (handle)
{
// this.DelayBind (() => {
// this.AddBindings(new Dictionary<object,string> ())
// });
}
public static PlainEntryCell Create ()
{
return (PlainEntryCell)Nib.Instantiate (null, null) [0];
}
public string CaptionText {
get {
return EntryTF.Text;
}
set {
EntryTF.Text = value;
RaisePropertyChanged("CaptionText");
}
}
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}

NavigationService throws NullReferenceException

Using MVVM Light, I'm trying to develop a rather simple WP7 application. I've run into a problem using the navigation service. I can navigate to a page, but after pressing the back button I can't navigate to the same page again. NavigationService throws a NullReferenceException.
I have implemented my navigation using Messaging from the GalaSoft.MvvmLight.Messaging namespace. All my views inherits from a customized PhoneApplicationPage base class that registrers a listener on "NavigationRequest":
public class PhoneApplicationPage : Microsoft.Phone.Controls.PhoneApplicationPage
{
public PhoneApplicationPage() : base()
{
Messenger.Default.Register<Uri>(this, "NavigationRequest", (uri) => NavigationService.Navigate(uri));
}
}
From my view models I post Uri's to this listener:
SendNavigationRequestMessage(new Uri("/View/AppSettingsView.xaml", UriKind.Relative));
Like i said, this works except when navigating after pressing the Back button.
Why is this and how can I solve it?
Is there a better way to implement navigation using MVVM Light?
I'm using MVVM Light as well. I have a class called PageConductor, which is based on what John Papa (Silverlight MVP) from Microsoft uses. Here's the PageConductor Service I use
public class PageConductor : IPageConductor
{
protected Frame RootFrame { get; set; }
public PageConductor()
{
Messenger.Default.Register<Messages.FrameMessage>(this, OnReceiveFrameMessage);
}
public void DisplayError(string origin, Exception e, string details)
{
string description = string.Format("Error occured in {0}. {1} {2}", origin, details, e.Message);
var error = new Model.Error() { Description = description, Title = "Error Occurred" };
Messenger.Default.Send(new Messages.ErrorMessage() { Error = error });
}
public void DisplayError(string origin, Exception e)
{
DisplayError(origin, e, string.Empty);
}
private void OnReceiveFrameMessage(Messages.FrameMessage msg)
{
RootFrame = msg.RootFrame;
}
private void Go(string path, string sender)
{
RootFrame.Navigate(new Uri(path, UriKind.Relative));
}
public void GoBack()
{
RootFrame.GoBack();
}
}
In my MainPage.xaml.cs constructor, I have this, which creates an instance of my ContentFrame in my PageConductor service.:
Messenger.Default.Send(new Messages.FrameMessage() { RootFrame = ContentFrame });
I then use dependency injection to instantiate an instance of my PageConductor Service into my MainPage ViewModel. Here is my MainViewModel class:
protected Services.IPageConductor PageConductor { get; set; }
public RelayCommand<string> NavigateCommand { get; set; }
public MainViewModel(Services.IPageConductor pageConductor)
{
PageConductor = pageConductor;
RegisterCommands();
}
private void RegisterCommands()
{
NavigateCommand = new RelayCommand<string>(
(source) => OnNavigate(source));
}
private void OnNavigate(string sender)
{
PageConductor.GoToView(sender, "main");
}
Notice the instance of my PageConductorService as a parameter in my MainViewModel constructor method. I pass this in via my ViewModelLocator:
private readonly TSMVVM.Services.ServiceProviderBase _sp;
public ViewModelLocator()
{
_sp = Services.ServiceProviderBase.Instance;
CreateMain(_sp);
}
#region MainPageViewModel
public static MainViewModel MainStatic
{
get
{
Services.ServiceProviderBase SP = Services.ServiceProviderBase.Instance;
if (_main == null)
{
CreateMain(SP);
}
return _main;
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",
"CA1822:MarkMembersAsStatic",
Justification = "This non-static member is needed for data binding purposes.")]
public MainViewModel Main
{
get
{
return MainStatic;
}
}
public static void ClearMain()
{
_main.Cleanup();
_main = null;
}
public static void CreateMain(Services.ServiceProviderBase SP)
{
if (_main == null)
{
_main = new MainViewModel(SP.PageConductor);
}
}
#endregion
For further reference, my Messages.FrameMessage class is simply:
internal class FrameMessage
{
public Frame RootFrame { get; set; }
}
I've had no issues with forward/back buttons.

Resources