Ξ Page Factory MVVM library for Xamarin.Forms

The main reason for making PageFactory was that I needed a very simple to use MVVM library which would free me from implementing the same things for any Xamarin.Forms project I created all over again. Those things were Page Caching, ViewModel oriented Navigation, INotifyPropertyChanged and ICommand implementations, Messaging between Pages and ViewModels. I also wanted my ViewModels to be dependency free (not forcing any concrete class inheritance).

That’s it. It’s very simple, no dependency injections, no platform specific code - just plain PCL. What comes with it, it’s very very lightweight. Here’s a blog note about it.

Ξ Table of contents

  1. Minimal code to get it working
  2. PageFactory Basics
  3. PageFactory Messaging
  4. PageFactory Navigation
  5. PageFactory INotifyPropertyChanged implementation
  6. PageFactory ICommand implementation
  7. Example showing basic features
  8. Summary

Project site: https://github.com/daniel-luberda/DLToolkit.PageFactory

Ξ Minimal code to get it working

Install nuget package from: https://www.nuget.org/packages/DLToolkit.PageFactory.Forms/

Ξ Simple “HelloWorld” example:

App.cs
1
2
3
4
5
6
7
8
9
public class App : Application
{

public App()
{

var pageFactory = new XamarinFormsPageFactory();
var navigationPage = pageFactory.Init<HelloViewModel, PFNavigationPage>();
MainPage = navigationPage;
}
}
HelloPage.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
public class HelloPage : PFContentPage<HelloViewModel>
{

public HelloPage()
{

var label = new Label() {
HorizontalOptions = LayoutOptions.CenterAndExpand,
VerticalOptions = LayoutOptions.CenterAndExpand,
};
label.SetBinding<HelloViewModel>(Label.TextProperty, v => v.HelloLabelText);

Content = label;
}
}
HelloViewModel.cs
1
2
3
4
5
6
7
8
9
10
11
12
public class HelloViewModel : BaseViewModel
{

public HelloViewModel()
{

HelloLabelText = "Hello World";
}

public string HelloLabelText {
get { return GetField<string>(); }
set { SetField(value); }
}
}

That’s all! You’ll have access to all PageFactory features!

Hello World

Ξ PageFactory Basics

Ξ Pages and ViewModels

PageFactory uses Pages and ViewModels.

  • Every Page implements IBasePage<INotifyPropertyChanged>, the generic argument is a ViewModel type
  • Every ViewModel must have a parameterless constructor and implement INotifyPropertyChanged (if only Page has to receive messages) or IBaseMessagable (if both Page and ViewModel have to receive messages). There are no other requirements.

Ξ PageFactory helper implementations

  • BaseViewModel for ViewModels. It implements INotifyPropertyChanged, IBaseMessagable) and has PageFactory property which returns PF.Factory instance.
  • BaseModel for Models. It implements INotifyPropertyChanged.
  • PageFactoryCommand, IPageFactoryCommand - PCL compatibile ICommand implementation.

Ξ PageFactory Factory instance

You can get PageFactory instance:

  • using PageFactory property in Page or ViewModel (if it inherits from BaseViewModel)
  • using static class property: PF.Factory

Some basic PageFactory methods you should know:

  • GetPageFromCache<TViewModel>() - Gets (or creates) cached Page instance.
  • GetMessagablePageFromCache<TViewModel>() - Gets (or creates) cached Page instance with ViewModel messaging support.
  • GetPageAsNewInstance<TViewModel>() - Creates a new Page Instance. New instance can replace existing cached Page instance (bool saveOrReplaceInCache argument).
  • GetMessagablePageAsNewInstance<TViewModel>() - Creates a new Page Instance with ViewModel messaging support. New instance can replace existing cached Page instance (bool saveOrReplaceInCache = false method parameter).

Cache can hold only one instance of ViewModel of the same type (with its Page). You can remove cache for a ViewModel type or replace it with another instance.

Ξ Fluent methods

ViewModels and Pages (IBasePage<INotifyPropertyChanged>) have some fluent style extensions. Instead:

Factory instance methods
1
2
3
4
5
6
7
8
var page = PageFactory.GetMessagablePageAsNewInstance<HomeViewModel>();
PageFactory.ResetPageViewModel(page);
PageFactory.SendMessageByPage(MessageConsumer.PageAndViewModel, page, "Message", this, "arg");
await PageFactory.PushPageAsync(page);

// Called from a ViewModel:
var page = PageFactory.GetPageByViewModel(this);
PageFactory.ReplacePageViewModel(page, new HomeViewModel());

You can use fluent style (I personally prefer it that way):

Fluent extensions
1
2
3
4
5
6
7
PageFactory.GetMessagablePageAsNewInstance<HomeViewModel>()
.ResetViewModel()
.SendMessageToPageAndViewModel("Message", this, "arg")
.PushPage();


// Called from a ViewModel:
this.GetPage().ReplaceViewModel(new HomeViewModel());

Ξ PageFactory Messaging

All PageFactory Pages have messaging enabled. If you also want ViewModel to receive messages it must implement IBaseMessagable interface (BaseViewModel does it).

Ξ Receiving messages

To receive messages just override PageFactoryMessageReceived method (either on Page or ViewModel):

Receiving messages
1
2
3
4
5
public override void PageFactoryMessageReceived(string message, object sender, object arg)
{

Console.WriteLine("HomeViewModel received {0} message from {1} with arg = {2}",
message, sender == null ? "null" : sender.GetType().ToString(), arg ?? "null")
;

}

Ξ Sending messages

To send messages use:

  • PageFactory static methods: SendMessageByPage, SendMessageByViewModel, SendMessageToCached
  • Page extension methods: SendMessageToPageAndViewModel, SendMessageToViewModel, SendMessageToPage
Sending messages (Fluent extensions)
1
2
3
4
5
PageFactory.GetMessagablePageAsNewInstance<HomeViewModel>()
.SendMessageToPageAndViewModel("Message", this, "arg")

PageFactory.GetPageFromCache<DetailsViewModel>()
.SendMessageToPage("Message", this, "arg");

Sending messages (Factory methods)
1
2
3
4
5
PageFactory.SendMessageToCached<HomeViewModel>(
MessageConsumer.PageAndViewModel, "Message", this, "arg")
;


var page = PageFactory.GetPageFromCache<DetailsViewModel>();
PageFactory.SendMessageToPage(Pages, "Message", this, "arg");

Ξ PageFactory Navigation

To navigate use this methods:

  • PageFactory static methods: PushPageAsync, PopPageAsync, InsertPageBefore, RemovePage, PopPagesToRootAsync, SetNewRootAndReset
  • Page extension methods: PushPage, PopPage, InsertPageBefore, RemovePage, PopPagesToRoot, SetNewRootAndReset
Navigating (Fluent extensions)
1
2
3
var page = PageFactory.GetPageAsNewInstance<DetailsPage>().PushPage();
await Task.Delay(5000);
page.PopPage();
Navigating (Factory methods)
1
2
3
4
var page = PageFactory.GetPageAsNewInstance<DetailsPage>();
await PageFactory.PushPageAsync(page);
await Task.Delay(5000);
await PageFactory.PopPageAsync();

You can intercept navigation. Just override one of this Page methods:

  • PageFactoryPushed, PageFactoryPopped, PageFactoryRemoved, PageFactoryInserted - called after successful navigation
  • PageFactoryPushing, PageFactoryPopping, PageFactoryRemoving, PageFactoryInserting - called before navigation. If false is returned, navigation will be cancelled
  • PageFactoryRemovingFromCache - called when the page is being removed from cache
Page Navigation override example
1
2
3
4
5
public override void PageFactoryPopped()
{

// Removes Page instance from PageFactory cache (it will be no longer needed)
this.RemovePageInstanceFromCache();
}
Page Navigation interception example
1
2
3
4
5
6
7
8
public override bool PageFactoryPushing()
{

// Page cannot be pushed if SomeCheckIsValid condition isn't met!!!
if (!SomeCheckIsValid)
return false;


return true;
}

Ξ PageFactory INotifyPropertyChanged implementation

Xamarin.Forms apps extensively use bindings. Because of that, every ViewModel should implement INotifyPropertyChanged interface. If you inherit from BaseViewModel class you can use its INotifyPropertyChanged implementation methods.

If you would like to use backing field:

Using backing field
1
2
3
4
5
6
string someExampleProperty;
public string SomeExampleProperty
{
get { return someExampleProperty; }
set { SetField(ref someExampleProperty, value); }
}

If you don’t want to use backing field you can use (It will store field value in internal Dictionary):

Storing field in internal Dictionary
1
2
3
4
5
public string SomeExampleProperty
{
get { return GetField<string>(); }
set { SetField(value); }
}

If you need to notify additional properties when your property changes, you can also do it with:

Using backing field and notyfing another properties changed
1
2
3
4
5
6
7
8
9
10
string someExampleProperty;
public string SomeExampleProperty
{
get { return someExampleProperty; }
set
{
SetField(ref someExampleProperty, value, () => SomeExampleProperty,
() => AnotherProperty1, () => AnotherProperty2);
}
}

or (It will store field value in internal Dictionary):

Storing field in internal Dictionary and notyfing another properties changed
1
2
3
4
5
6
7
8
9
public string SomeExampleProperty
{
get { return GetField<string>(); }
set
{
SetField(value, () => SomeExampleProperty,
() => AnotherProperty1, () => AnotherProperty2);
}
}

You can also use Fody INotifyPropertyChanged (https://github.com/Fody/PropertyChanged) and just write:

Fody INotifyPropertyChanged
1
public string SomeExampleProperty { get; set; }

Don’t forget to add “FodyWeavers.xml” file to you project (BuildAction set to Content):

FodyWeavers.xml
1
2
3
4
<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
<PropertyChanged/>
</Weavers>

And that’s it! Fody will auto-implement all INotifyPropertyChanged classes!

Ξ PageFactory ICommand implementation

PageFactoryCommand Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public TestViewModel()
{

TestWithoutParamCommand = new PageFactoryCommand(() => {
Console.WriteLine("TestWithoutParamCommand executed");
});

TestWithParamCommand = new PageFactoryCommand<string>((param) => {
Console.WriteLine("TestWithParamCommand executed with param={0}", param);
}, (obj) => ShouldExecute(obj));
}

IPageFactoryCommand TestWithoutParamCommand { get; set; }

IPageFactoryCommand TestWithParamCommand { get; set; }

public bool ShouldExecute(string obj)
{

return !string.IsNullOrEmpty(obj);
}

Then you could use it (XAML example):

1
2
<Button Text="Execute TestWithoutParamCommand"
Command="{Binding TestWithoutParamCommand}"/>

Output after button tap: TestWithoutParamCommand executed (will always execute)

1
2
3
<Button Text="Execute TestWithParamCommand"
Command="{Binding TestWithParamCommand}"
CommandParameter="someParameterValue"/>

Output after button tap: TestWithParamCommand executed with param=someParameterValue

1
2
3
<Button Text="Execute TestWithParamCommand"
Command="{Binding TestWithParamCommand}"
CommandParameter=""/>

Output after button tap: Won’t execute

To notify command that CanExecute changed use PageFactory RaiseCanExecuteChanged(); method.

Ξ Example showing basic features

Ξ Flow:

App starts up ➡ User taps "Open Details Page" button ➡ User taps "Lock access & return to HomePage" button

Example

Ξ Details:

  1. App starts up
  • "Open Details Page" button is enabled
  • "DetailsPage is accessible" text is shown inside HomePage
  1. User taps "Open Details Page" button
  • HomePage is created and put into PageFactory cache
  • HomeViewModel sends "WarningMessage" message to DetailsViewModel (with warning message string argument)
  • DetailsViewModel receives and handles received message (sets text to "When you lock this page, you’ll not be able to open it again")
  • DetailsPage is pushed
  • Received "When you lock this page, you’ll not be able to open it again" text is shown inside DetailsPage
  1. User taps "Lock access & return to HomePage" button
  • DetailsViewModelsends "DetailsPageAccess" message to HomeViewModel with bool argument set to false
  • HomeViewModel receives receives and handles received message (disables button command and sets text which is shown inside HomePage)
  • DetailsPage is popped and forced to be removed from PageFactory cache
  • "DetailsPage is locked" text is shown inside HomePage

Ξ Code:

HomeViewModel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class HomeViewModel : BaseViewModel
{

public HomeViewModel()
{

PageTitle = "HomePage";
HomeLabelText = "DetailsPage is accessible";
CanOpenDetailsPage = true;

OpenDetailsPageCommand = new PageFactoryCommand(() => {

var messageToSend = "When you lock this page, you'll not be able to open it again";

PageFactory.GetMessagablePageFromCache<DetailsViewModel>()
.ResetViewModel()
.SendMessageToViewModel(message: "WarningMessage", sender: this, arg: messageToSend)
.PushPage();


}, () => CanOpenDetailsPage);
}

public override void PageFactoryMessageReceived(string message, object sender, object arg)
{

if (message == "DetailsPageAccess")
{

CanOpenDetailsPage = (bool)arg;
HomeLabelText = "DetailsPage is locked";
}

Console.WriteLine("HomeViewModel received {0} message from {1} with arg = {2}",
message, sender == null ? null : sender.GetType().ToString(), arg ?? "null")
;

}

public IPageFactoryCommand OpenDetailsPageCommand { get; private set; }

public bool CanOpenDetailsPage {
get { return GetField<bool>(); }
set
{
if (SetField(value) && OpenDetailsPageCommand != null)
OpenDetailsPageCommand.RaiseCanExecuteChanged();
}
}

public string HomeLabelText {
get { return GetField<string>(); }
set { SetField(value); }
}

public string PageTitle
{
get { return GetField<string>(); }
set { SetField(value); }
}
}
HomePage.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class HomePage : PFContentPage<HomeViewModel>
{

public HomePage()
{

this.SetBinding<HomeViewModel>(Page.TitleProperty, v => v.PageTitle);

var label = new Label() {
HorizontalOptions = LayoutOptions.CenterAndExpand,
VerticalOptions = LayoutOptions.CenterAndExpand,
};

label.SetBinding<HomeViewModel>(Label.TextProperty, v => v.HomeLabelText);

var button = new Button() {
HorizontalOptions = LayoutOptions.FillAndExpand,
Text = "Open Details Page"
};

button.SetBinding<HomeViewModel>(Button.CommandProperty, v => v.OpenDetailsPageCommand);

Content = new StackLayout {
Children = {
label,
button
}
};
}
}
DetailsViewModel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class DetailsViewModel : BaseViewModel
{

public DetailsViewModel()
{

PageTitle = "DetailsPage";

PopDetailsPageCommand = new PageFactoryCommand(() => {

PageFactory.GetMessagablePageFromCache<HomeViewModel>()
.SendMessageToViewModel(message: "DetailsPageAccess", sender: this, arg: false);


this.GetPage().PopPage();
});
}

public override void PageFactoryMessageReceived(string message, object sender, object arg)
{

Console.WriteLine("DetailsViewModel received {0} message from {1} with arg = {2}",
message, sender == null ? null : sender.GetType().ToString(), arg ?? "null")
;


if (message == "WarningMessage")
{

var receivedMessage = (string)arg;
DetailsLabelText = receivedMessage;
}
}

public IPageFactoryCommand PopDetailsPageCommand { get; private set; }

public string DetailsLabelText {
get { return GetField<string>(); }
set { SetField(value); }
}

public string PageTitle
{
get { return GetField<string>(); }
set { SetField(value); }
}
}
DetailsPage.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class DetailsPage : PFContentPage<DetailsViewModel>
{

public DetailsPage()
{

this.SetBinding<DetailsViewModel>(Page.TitleProperty, v => v.PageTitle);

var label = new Label() {
HorizontalOptions = LayoutOptions.CenterAndExpand,
VerticalOptions = LayoutOptions.CenterAndExpand,
};
label.SetBinding<DetailsViewModel>(Label.TextProperty, v => v.DetailsLabelText);

var button = new Button() {
HorizontalOptions = LayoutOptions.FillAndExpand,
Text = "Lock access & return to HomePage"
};
button.SetBinding<DetailsViewModel>(Button.CommandProperty, v => v.PopDetailsPageCommand);

Content = new StackLayout {
Children = {
label,
button
}
};
}

public override void PageFactoryPopped()
{

// Removes Page instance from PageFactory cache (it will be no longer needed)
this.RemovePageInstanceFromCache();
}
}

Ξ Summary

That’s all for now. Feel free to test it out and notify me about any errors / your suggestions. Thanks!