Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

In this tutorial, you're going to learn how to create a simple to-do app which syncs data from one client to another with the help of SQLite and Crosslight synchronization framework integrated to OS-level synchronization (SyncAdapter on Android and Background App Refresh on iOS). This way, you can create a simple to-do app that syncs between iOS and Android. This tutorial is a continuation of the the previous tutorial: Walkthrough: Create a To-Do App with Data Synchronization. At the end of this tutorial, you should have the following result.

Panel

Samples:

CrosslightToDoOSSync.zip

Follow these steps:

Table of Contents
maxLevel3
stylecircle

Anchor
preqreuisite
preqreuisite

Prerequisite

Before starting the walkthrough, it is recommended that you have followed these walkthroughs in order:

It is also recommended that you have read through these conceptual topics in order to get a better understanding:

To use this walkthrough, you will need to use at least Crosslight version 5.0.5000.626-experimental in order to achieve the desired result. You also need to have Mobile Studio for Windows installed. If you haven't done so, download here.

Let's get started. 

Preparing the Project

In this tutorial, we're going to continue from the previous tutorial, where you've learned how to perform various data operations to SQLite storage. Download the sample here (from the previous tutorial). After you've downloaded the sample, you're ready to proceed.

We're going to modify the current database models and EDMX to associate each ToDo entry with a user. This requires us to change the app design a bit. For instance, when the user launches the app for the first time, we're going to check if the user has logged in to the application. If not, we'll present a modal screen that prompts user to enter his/her username before using the application. Then, upon each synchronization attempt in the server, we'll send a push notification to the other registered device, which will then trigger an auto-synchronization process by utilizing the background sync capabilities of each platform (SyncAdapter on Android and Background App Refresh on iOS).

Modifying the Database

Before attempting the said action, we'll need to change the database design a bit. Open the CrosslightDb.mdf inside CrosslightToDo.WebAPI/AppData folder. When you try to open the database, you might get the following error.

To resolve this issue, it's pretty simple. All you need to do is open Web.config file inside CrosslightToDo.WebAPI project and change the following connection string.

Into

Code Block
languagexml
data source=(LocalDb)\MSSQLLocalDB

Which should look like the following.

Now you can open the MDF file. Right-click and Add New Table.

Use the following query.

Code Block
languagesql
CREATE TABLE [dbo].[ToDo] (
    [Id]          UNIQUEIDENTIFIER NOT NULL,
    [Text]        NVARCHAR (MAX)   NULL,
    [IsCompleted] BIT              DEFAULT ((0)) NOT NULL,
    [CreatedOn]   DATETIME         NULL,
    [ModifiedOn]  DATETIME         NULL,
    [IsDeleted]   BIT              DEFAULT ((0)) NOT NULL,
    [CreatedBy] NVARCHAR(128) NOT NULL, 
    PRIMARY KEY CLUSTERED ([Id] ASC), 
    CONSTRAINT [FK_ToDo_Users] FOREIGN KEY ([CreatedBy]) REFERENCES [Users]([UserId])
);

Delete the other two unused databases.

Delete these three connection strings inside CrosslightToDo.WebApi/Web.config.

Next, open app.config inside CrosslightToDo.DomainModels project.

Delete the connection string here as well.

Delete the four files here as well.

Then re-add a new ToDo.edmx file, just the same way as you would in the previous tutorial. Don't forget to enable the Intersoft WebAPI Extension as well as setting other properties such as Code Generation Version, Repository Type, Logical Delete Property, and Synchronization Date Property. Also, when you're done, don't forget to configure the IIS Express binding as well so that the server is exposed to your local network.

Creating Login Page

Before we can test the new CreatedBy column, we need to create a login page for the user, if the user tries to use the app without logging in first. Open AppService.cs under CrosslightToDo.Core/Infrastructure folder and use the following code.

Code Block
languagec#
using System.Reflection;
using CrosslightToDo.DomainModels.ToDo;
using CrosslightToDo.ViewModels;
using Intersoft.AppFramework;
using Intersoft.AppFramework.Identity;
using Intersoft.AppFramework.ModelServices;
using Intersoft.AppFramework.PushNotification;
using Intersoft.AppFramework.Services;
using Intersoft.Crosslight;
using Intersoft.Crosslight.Containers;
using Intersoft.Crosslight.Data.EntityModel;
using Intersoft.Crosslight.RestClient;
using Intersoft.Crosslight.Services;
using Intersoft.Crosslight.Services.Auth;
using Intersoft.Crosslight.Services.PushNotification;
using Intersoft.AppFramework.Models;
using Intersoft.Crosslight.Data.ComponentModel;
using System;
using System.Threading.Tasks;
namespace CrosslightToDo.Infrastructure
{
    /// <summary>
    ///     Crosslight's shared application initializer.
    ///     This is the perfect place to register repositories, custom services, and other dependencies via IoC.
    /// </summary>
    /// <seealso cref="Intersoft.Crosslight.ApplicationServiceBase" />
    public sealed class CrosslightAppAppService : ApplicationServiceBase
    {
        #region Constructors
        /// <summary>
        ///     Initializes a new instance of the <see cref="CrosslightAppAppService" /> class.
        /// </summary>
        /// <param name="context">
        ///     The application context that implements <see cref="T:Intersoft.Crosslight.IApplicationContext" />
        /// </param>
        public CrosslightAppAppService(IApplicationContext context)
            : base(context)
        {
            AppSettings appSettings = new AppSettings();
            appSettings.LocalDatabaseName = "todo.db3";
            appSettings.LocalDatabaseLocation = LocalFolderKind.Data;
            //http://10.211.55.4:18177/data/ToDo/ToDoes
            appSettings.WebServerUrl = "http://10.211.55.4:18177/";
            appSettings.BaseAppUrl = appSettings.WebServerUrl;
            appSettings.RestServiceUrl = appSettings.BaseAppUrl + "/data/ToDo";
            appSettings.IdentityServiceUrl = appSettings.BaseAppUrl + "/data/Identity";
            appSettings.PushNotificationServiceUrl = appSettings.BaseAppUrl + "/data/PushNotification";
            appSettings.RequiresInternetConnection = true;
            appSettings.EnableDataSynchronization = true;
            appSettings.DataSynchronizationMode = DataSynchronizationMode.LoadAll;
            // add new services (extensions)
            ServiceProvider.AddService<IUserService, UserService>();
            ServiceProvider.AddService<IAccountService, WebApiAccountService>();
            ServiceProvider.AddService<IAuthenticationService, AuthenticationService>();
            ServiceProvider.AddService<IPushNotificationService, PushNotificationService>();
            ServiceProvider.AddService<IPushRegistrationService, PushRegistrationService>();
            // shared services registration
            this.GetService<ITypeResolverService>().Register(typeof(CrosslightAppAppService).GetTypeInfo().Assembly);
            // components specific registration
            this.GetService<IActivatorService>().Register<IRestClient>(c =>
            {
                RestClient restClient = new RestClient(appSettings.RestServiceUrl);
                restClient.TypeResolver = new EntityTypeResolver();
                restClient.AuthenticationServiceId = this.AccountService.ServiceId;
                restClient.AuthenticationUrl = appSettings.AuthenticationUrl;
                restClient.Account = this.AccountService.GetAccount();
                return restClient;
            });
            Container.Current.RegisterInstance(appSettings);
            // data sync service
            if (appSettings.EnableDataSynchronization)
            {
                if (appSettings.DataSynchronizationMode == DataSynchronizationMode.LoadAll)
                {
                    Container.Current.Register<IEntityContainer>("Default", c => new EntityContainer()
                    {
                        EnableSynchronization = true
                    }).WithLifetimeManager(new ContainerLifetime());
                }
                else
                {
                    Container.Current.Register<IEntityContainer>("Default", c => new LocalEntityContainer()
                    {
                        EnableSynchronization = true
                    }).WithLifetimeManager(new ContainerLifetime());
                }
            }
            else
                Container.Current.Register<IEntityContainer>("Default", c => new EntityContainer()).WithLifetimeManager(new ContainerLifetime());
            Container.Current.Register<IUserRepository, UserRepository>();
            Container.Current.Register<IPushNotificationRepository, PushNotificationRepository>();
            Container.Current.Register<IToDoRepository>((c) => new ToDoRepository(c.Resolve<IEntityContainer>("Default")));
            // local data storage service
            ServiceProvider.AddService<ISQLiteService, SQLiteService>();
            // data sync service
            ServiceProvider.AddService<ISynchronizationService>((c) =>
            {
                IEntityContainer container = Container.Current.Resolve<IEntityContainer>("Default");
                ISynchronizationService service = new SynchronizationService(container);
                service.SynchronizationTypes = ToDoEntities.SynchronizationTypes;
                // Uncomment the following line to show verbose sync messages, ideal for debugging purpose
                // service.ShowStatus = true;
                return service;
            });

        }
        #endregion
        #region Properties
        public IAccountService AccountService => ServiceProvider.GetService<IAccountService>();
        private bool IsDataSynchronizationEnabled
        {
            get { return Container.Current.Resolve<AppSettings>().EnableDataSynchronization; }
        }
        private ISynchronizationService SynchronizationService
        {
            get { return this.GetService<ISynchronizationService>(); }
        }
        #endregion
        #region Methods
        protected override void OnResume()
        {
            base.OnResume();
            this.OnSync(null);
        }
        /// <summary>
        ///     Called when the application is starting.
        /// </summary>
        /// <param name="parameter">The startup parameters.</param>
        protected override void OnStart(StartParameter parameter)
        {
            base.OnStart(parameter);
            this.AccountService.Initialize(typeof(LoginViewModel));
            //Specify the first ViewModel to use when launching the application.
            this.SetRootViewModel<SimpleViewModel>();
        }
        protected override void OnSync(SyncContext context)
        {
            try
            {
                base.OnSync(context);
                if (this.IsDataSynchronizationEnabled)
                {
                    if (this.SynchronizationService.DefaultQueryDefinition == null)
                    {
                        var service = this.GetService<IUserService>();
                        var task = TaskEx.Run(async () => await service.GetCachedUserAsync());
                        var user = task.WaitForResult();
                        if (user != null)
                            service.SetCurrentUser(user);
                        var queryDefinition = new LocalTypeQueryDefinition();
                        var queryDescriptor = new QueryDescriptor();
                        queryDescriptor.FilterDescriptors.Add(new FilterDescriptor("CreatedBy", FilterOperator.IsEqualTo, user.Id));
                        queryDefinition.AddQuery(typeof(ToDo), queryDescriptor);
                        // set as the default query for synchronization service
                        this.SynchronizationService.DefaultQueryDefinition = queryDefinition;
                    }
                    if (this.AccountService != null && this.AccountService.IsLoggedIn())
                        this.SynchronizationService.SynchronizeDataAsync();
                }
            }
            catch (Exception ex)
            {
                var presenterService = this.GetService<IPresenterService>();
                if (presenterService != null)
                {
                    var toastPresenter = presenterService.GetPresenter<IToastPresenter>();
                    if (toastPresenter != null)
                        toastPresenter.Show(ex.Message);
                }
            }
        }
        #endregion
    }
}

There are several notable changes inside AppService.cs, in the constructor, we've added several additional configurations to AppSettings, which defines the IdentityServiceUrl for the login target. The RestClient also receives additional configurations to allow authenticated requests. We've also initialized the EntityContainer which is an essential component for the synchronization process. Here, we've also added a new overridden method OnSync, which contains the logic for the sync process. We've also provided additional QueryDescriptor for the sync process to retrieve the ToDos based on the logged in user. When the app resumes from its suspended state, we call the OnSync one more time.

Proceed by adding a new ViewModel under CrosslightToDo.Core/ViewModels folder called LoginViewModel. Use the following code.

Code Block
languagec#
using Intersoft.AppFramework.Identity;
using Intersoft.Crosslight;
using Intersoft.Crosslight.Input;
using Intersoft.Crosslight.Services.Auth;
using Intersoft.Crosslight.ViewModels;
namespace CrosslightToDo.ViewModels
{
    public class LoginViewModel : ViewModelBase
    {
        #region Constructors
        public LoginViewModel()
        {
            this.LoginCommand = new DelegateCommand(ExceuteLogin);
            this.Title = "Login";
        }
        #endregion
        #region Fields
        private string _txtUsername;
        #endregion
        #region Properties
        public IAccountService AccountService => ServiceProvider.GetService<IAccountService>();
        public DelegateCommand LoginCommand { get; set; }
        public string TxtUsername
        {
            get { return _txtUsername; }
            set
            {
                if (_txtUsername != value)
                {
                    _txtUsername = value;
                    OnPropertyChanged("TxtUsername");
                }
            }
        }
        public IUserService UserService => ServiceProvider.GetService<IUserService>();
        #endregion
        #region Methods
        private async void ExceuteLogin(object parameter)
        {
            this.ActivityPresenter.Show("Logging you in..");
            IAccount account = this.AccountService.CreateEncryptedAccount(this.TxtUsername, this.TxtUsername);
            try
            {
                //try registering the user
                await this.UserService.RegisterAsync(new RegistrationData()
                {
                    FirstName = this.TxtUsername,
                    LastName = this.TxtUsername,
                    Password = this.TxtUsername,
                    PasswordHash = account.GetProperty(Account.PasswordHash),
                    UserName = this.TxtUsername,
                    Email = this.TxtUsername + "@" + this.TxtUsername + ".com"
                });
            }
            catch (AuthenticationException e)
            {
                //if the registration fails and user already exists, do nothing
            }
            finally
            {
                await this.AccountService.SignInAsync(account);
                this.ActivityPresenter.Hide();
                this.NavigationService.Close();
            }
        }
        #endregion
    }
}

Here, we have two simple properties, which are TxtUsername as well as LoginCommand that will be used for the login process. When the user performs login, in the ExecuteLogin method, we utilize the AccountService registered in the AppService to create an encrypted account, then try to register the user. If the user already exists, then we simply sign in. Since this is used for demonstration purposes, this is not considered as best practice. This is just to show how you can sync ToDo items that will be associated to a user easily. 

Create a new Crosslight Binding Provider called LoginBindingProvider and put it inside CrosslightToDo.Core/BindingProviders folder. Use the following code.

Code Block
languagec#
using Intersoft.Crosslight;
namespace CrosslightToDo
{
    public class LoginBindingProvider : BindingProvider
    {
        #region Constructors
        public LoginBindingProvider()
        {
            this.AddBinding("TxtUsername", BindableProperties.TextProperty, new BindingDescription("TxtUsername", BindingMode.TwoWay){UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged});
            this.AddBinding("BtnLogin", BindableProperties.CommandProperty, "LoginCommand");
        }
        #endregion
    }
}

The BindingProvider contains nothing except for the text box that will be bound to the TxtUsername property in the LoginViewModel as well as the button to the LoginCommand property.

Proceed by creating a new Crosslight Android Material Fragment called LoginFragment inside CrosslightToDo.Android/Fragments folder. Use the following code.

Code Block
languagec#
using CrosslightToDo.ViewModels;
using Android.Runtime;
using Intersoft.Crosslight;
using Intersoft.Crosslight.Android.v7;
using System;
namespace CrosslightToDo.Android
{
    [ImportBinding(typeof(LoginBindingProvider))]
    public class LoginFragment : Fragment<LoginViewModel>
    {
        #region Constructors
        public LoginFragment()
        {
        }
        public LoginFragment(IntPtr javaReference, JniHandleOwnership transfer)
            : base(javaReference, transfer)
        {
        }
        #endregion
        #region Properties
        protected override int ContentLayoutId
        {
            get { return Resource.Layout.login_layout; }
        }
        protected override bool ShowActionBarUpButton
        {
            get
            {
                return false;
            }
        }
        #endregion
    }
}

This is just a simple layout that will be inflated for the login screen. Here, the action bar up button is also hidden. Proceed by creating the login_layout.axml inside CrosslightToDo.Android/Resources folder and use the following code.

Code Block
languagexml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="10dp">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Crosslight To-Do"
        android:textAlignment="center"
        android:gravity="center"
        android:textSize="24sp" />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Before you can use this application, you'll need to provide a username first."
        android:textAlignment="center"
        android:gravity="center"
        android:layout_marginTop="30dp" />
    <EditText
        android:id="@+id/TxtUsername"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:hint="Username"
        android:layout_marginTop="30dp" />
    <Button
        android:id="@+id/BtnLogin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Login" />
</LinearLayout>

This is just a simple layout for the login screen which contains an EditText as well as a Button. Then modify the SimpleViewModel.cs inside CrosslightToDo.Core/ViewModels folder and use the following code.

Code Block
languagec#
using System;
using CrosslightToDo.Core;
using CrosslightToDo.DomainModels.ToDo;
using Intersoft.AppFramework;
using Intersoft.AppFramework.Identity;
using Intersoft.AppFramework.Services;
using Intersoft.AppFramework.ViewModels;
using Intersoft.Crosslight;
namespace CrosslightToDo.ViewModels
{
    public class SimpleViewModel : DataListViewModelBase<ToDo, IToDoRepository>
    {
        #region Constructors
        /// <summary>
        ///     Initializes a new instance of the <see cref="SimpleViewModel" /> class.
        /// </summary>
        public SimpleViewModel()
        {
            this.Title = "My To-Do List";
            this.EnableRefresh = true;
        }
        #endregion
        #region Properties
        public IAccountService AccountService => ServiceProvider.GetService<IAccountService>();
        public AppSettings AppSettings => Container.Current.Resolve<AppSettings>();
        public bool IsLoginDisplayed { get; set; }
        public ISynchronizationService SynchronizationService => ServiceProvider.GetService<ISynchronizationService>();
        public IUserService UserService => ServiceProvider.GetService<IUserService>();
        protected override IQueryDefinition ViewQuery
        {
            get { return new ToDoQueryDefinition(); }
        }
        #endregion
        #region Methods
        private void DisplayLogin()
        {
            if (!this.IsLoginDisplayed)
            {
                this.IsLoginDisplayed = true;
                IViewService viewService = this.GetService<IViewService>();
                if (viewService != null)
                {
                    viewService.RunOnBackgroundThread(() =>
                        {
                            viewService.RunOnUIThread(() =>
                                {
                                    this.NavigationService.Navigate<LoginViewModel>(new NavigationParameter(NavigationMode.Modal), async result =>
                                        {
                                            if (!this.AccountService.IsLoggedIn())
                                            {
                                                this.DisplayLogin();
                                                this.ToastPresenter.Show("Please login before using the app.");
                                                this.IsLoginDisplayed = false;
                                            }
                                            else
                                                await this.SynchronizationService.SynchronizeDataAsync(SynchronizeAction.LoadLocalDataFollowedWithSyncData);
                                        });
                                });
                        }, 2000);
                }
            }
        }
        protected override void ExecuteAdd(object parameter)
        {
            base.ExecuteAdd(parameter);
            this.DialogPresenter.Show<AddToDoViewModel>(new DialogOptions("Add New Item"), async result =>
            {
                User user = await this.UserService.GetUserAsync(this.AccountService.GetAccount());
                if (user != null)
                {
                    AddToDoViewModel viewModel = result.ViewModel as AddToDoViewModel;
                    if (viewModel != null)
                    {
                        if (string.IsNullOrEmpty(viewModel.ToDoText))
                            this.ToastPresenter.Show("You haven't entered any text.");
                        else
                        {
                            ToDo todo = new ToDo
                            {
                                Id = Guid.NewGuid(),
                                Text = viewModel.ToDoText,
                                CreatedOn = DateTime.Now,
                                CreatedBy = user.Id
                            };
                            this.Repository.Insert(todo);
                            //Important: call this to refresh the table view
                            this.OnDataInserted(todo);
                            this.SaveAndSynchronize();
                        }
                    }
                }
            });
        }
        protected override async void ExecuteEditAction(object parameter)
        {
            EditingParameter editingParameter = parameter as EditingParameter;
            if (editingParameter != null)
            {
                ToDo todo = editingParameter.Item as ToDo;
                string editAction = editingParameter.CustomAction;
                if (todo != null)
                {
                    if (editAction == "Delete")
                    {
                        this.Repository.Delete(todo);
                        //Important: call this to refresh the table view
                        this.OnDataRemoved(editingParameter.Item as ToDo);
                        this.SaveAndSynchronize();
                    }
                    else if (editAction == "Complete")
                    {
                        todo.IsCompleted = true;
                        //Important: call this to refresh the table view
                        this.OnDataChanged(todo);
                        this.SaveAndSynchronize();
                    }
                    editingParameter.ShouldEndEditing = true;
                }
            }
        }
        public override async void Navigated(NavigatedParameter parameter)
        {
            base.Navigated(parameter);
            bool isSignedIn = this.AccountService.IsLoggedIn();
            if (!isSignedIn)
                this.DisplayLogin();
            else
            {
                User user = await this.UserService.GetCachedUserAsync();
                this.UserService.SetCurrentUser(user);
                this.SynchronizationService.SynchronizeDataAsync(SynchronizeAction.LoadLocalDataFollowedWithSyncData);
            }
        }
        private async void SaveAndSynchronize()
        {
            await this.Repository.SaveChangesAsync();
            await this.SynchronizationService.SynchronizeDataAsync();
        }
        #endregion
    }
}

The ViewModel is pretty much unchanged except now when adding a ToDo item, we also specify the CreatedBy property. In the Navigated method, we've also detected whether the user has performed login or not. If not, then we'll display the LoginViewModel that prompts the user for a username to be used with the application. 

Now that you've prepared the login screen for Android, let's prepare the one for iOS. Begin by adding a new Crosslight View Controller, give it a name of LoginViewController and use the following code.

Code Block
languagec#
using Intersoft.Crosslight;
using Intersoft.Crosslight.iOS;
using CrosslightToDo.ViewModels;
using System;
namespace CrosslightToDo.iOS
{
    [Storyboard("MainStoryboard")]
    [ImportBinding(typeof(LoginBindingProvider))]
    public partial class LoginViewController : UIViewController<LoginViewModel>
    {
        public LoginViewController(IntPtr intPtr)
            : base(intPtr)
        {
        }
    }
}

As you can see, this is just an empty ViewController that will be used for the login screen. Next, open up MainStoryboard.storyboard inside CrosslightToDo.iOS/Views folder. Create a new ViewController for the LoginViewController. Up to this point, it is assumed that you have a working knowledge of how iOS storyboard works, due to prior experiences in following previous tutorials.

Simply create a UITextField for the user to enter his/her username, as well as a button with both outlets set to TxtUsername and BtnLogin.

Run the server, then try to run the application on simulators, and you should get the following result.

Now that you've successfully created the login screen, simply input any username of your choice and create a new to-do item. Once you've done that, go back to Visual Studio and check your database to see if the ToDo is successfully created and associated to a user. You should get a result similar to the following. 

As you can see, now you've successfully created a ToDo list that is associated with a user and retains the same behavior as the previous application.

Video
Autoplayfalse
Sourcehttp://developer.intersoftsolutions.com/download/attachments/27297667/synced-todo.mp4?api=v2
Width400px

However, in this occasion, we would like to enhance this experience further by automatically updating the other device with new data with the help of push notifications and OS-level synchronization components (SyncAdapter on Android and Background App Refresh on iOS). Let's do this next.

Configuring Push Notifications

The next step to accomplish automated syncing is to enable push notifications across these devices. Start by modifying the SimpleViewModel.cs and add the following property.

Code Block
languagec#
public IPushRegistrationService PushRegistrationService => ServiceProvider.GetService<IPushRegistrationService>();

Then modify the following code.

Code Block
languagec#
private void DisplayLogin()
{
    if (!this.IsLoginDisplayed)
    {
        this.IsLoginDisplayed = true;
        IViewService viewService = this.GetService<IViewService>();
        if (viewService != null)
        {
            viewService.RunOnBackgroundThread(() =>
                {
                    viewService.RunOnUIThread(() =>
                        {
                            this.NavigationService.Navigate<LoginViewModel>(new NavigationParameter(NavigationMode.Modal), async result =>
                                {
                                    if (!this.AccountService.IsLoggedIn())
                                    {
                                        this.DisplayLogin();
                                        this.ToastPresenter.Show("Please login before using the app.");
                                        this.IsLoginDisplayed = false;
                                    }
                                    else
                                    {
                                        await this.SynchronizationService.SynchronizeDataAsync(SynchronizeAction.LoadLocalDataFollowedWithSyncData);
                                        await this.PushRegistrationService.RegisterPushNotificationAsync();
                                    }
                                });
                        });
                }, 2000);
        }
    }

Here, in the DisplayLogin method, we perform register for push notifications. Also modify the following method.

Code Block
languagec#
public override async void Navigated(NavigatedParameter parameter)
{
    base.Navigated(parameter);
    bool isSignedIn = this.AccountService.IsLoggedIn();
    if (!isSignedIn)
        this.DisplayLogin();
    else
    {
        User user = await this.UserService.GetCachedUserAsync();
        this.UserService.SetCurrentUser(user);
        await this.SynchronizationService.SynchronizeDataAsync(SynchronizeAction.LoadLocalDataFollowedWithSyncData);
        await this.PushRegistrationService.RegisterPushNotificationAsync();
    }
}

Next, open up AppService.cs inside CrosslightToDo.Core/Infrastructure folder and add the following code.

Code Block
languagec#
protected async override void OnDeviceTokenReceived(DeviceToken deviceToken)
{
    base.OnDeviceTokenReceived(deviceToken);
    var pushRegistrationService = ServiceProvider.GetService<IPushRegistrationService>();
    var userService = this.GetService<IUserService>();
    var user = userService.GetCurrentUser();
    // device token is received from Platform Store Service
    // now check if the token and user has been registered in our app
    if (await pushRegistrationService.ShouldRegisterDeviceTokenAsync(deviceToken))
    {
        await pushRegistrationService.SaveDeviceTokenAsync(deviceToken, user, false);
        await pushRegistrationService.RegisterDeviceTokenAsync(deviceToken, user);
    }
}

This will handle when the device received device token from the Google and store it into our database. Next, we're going to configure the server to allow push notifications. Open up Google Cloud Console

Select Create a project as shown in the shot above. Give an appropriate name for the project then select Create.

Once your project is created, choose Enable and manage APIs.

Then in the next screen, choose Google Cloud Messaging.

In the next screen, click on Enable.

Next, click on Go to Credentials.

In the next screen, from the dropdown, choose Web Server. Then click on What credentials do I need?

Then give an appropriate name for your key and click on Create API key.

In the next screen, you'll get your server API key. Copy this value, click Done, and open Global.asax.cs inside CrosslightToDo.WebAPI project.

At the bottom, you should see a code similar to the following. Insert your copied server key onto the placeholder.

Now you've successfully configured the server for Google Push Notifications. Also ensure that the following ports are not blocked on your network: 5229, 5229, 5230. Next we're going to configure the server for Apple push notification. Please follow step-by-step guidance from this documentation: Preparing Apple Push Notification ServiceOnce you've followed through the documentation, ultimately, you'll end up with a private key on your WebAPI project, as follows. Don't forget to set the Build Action as Embedded Resource.

Once you've done that, open Global.asax.cs inside CrosslightToDo.WebAPI project and uncomment this line.

Insert the following code block.

Code Block
languagec#
PushServiceManager.Instance.RegisterAppleService(new ApplePushChannelSettings(new X509Certificate2(Server.MapPath("IntersoftPrivateKey.p12"), "intersoft")), "CrosslightToDo.iOS");

Note that you may need to configure the password as necessary. In this example, it's intersoft.

Sending Push Notifications from Server

Now that you've successfully configured push notifications from the server, we need to modify some codes that will trigger the push notifications each time changes to ToDo data is made. Create a new controller inside CrosslightToDo.WebAPI/Controllers folder. 

 


Give it a name of ToDoController.cs.

Use the following code.

Code Block
languagec#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using CrosslightToDo.DomainModel;
using CrosslightToDo.DomainModels.PushNotification;
using Intersoft.Data.WebApi;
using Intersoft.Messaging.PushService;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
namespace CrosslightToDo.DomainModels.ToDo.Controllers
{
    public partial class ToDoController
    {
        #region Constructors
        public ToDoController()
        {
            this.Db.AfterSaveEntitiesDelegate = AfterSaveEntitiesDelegate;
            this.Db.BeforeExecuteQueryDelegate = BeforeExecuteQueryDelegate;
        }
        #endregion
        #region Fields
        private PushNotificationEntities _notificationContext;
        #endregion
        #region Properties
        public PushNotificationEntities NotificationContext
        {
            get
            {
                if (_notificationContext == null)
                    _notificationContext = new PushNotificationEntities();
                return _notificationContext;
            }
        }
        #endregion
        #region Methods
        private void AfterSaveEntitiesDelegate(Dictionary<Type, List<EntityInfo>> saveMap, List<KeyMapping> keyMappings)
        {
            if (saveMap.Any() && saveMap.ContainsKey(typeof(ToDo)))
            {
                if (HttpContext.Current != null)
                {
                    UserPrincipal principal = HttpContext.Current.User as UserPrincipal;
                    UserIdentity userIdentity = principal?.Identity as UserIdentity;
                    if (userIdentity != null)
                    {
                        User user = this.GetUser(userIdentity.Name);
                        PushServiceManager manager = PushServiceManager.Instance;
                        string deviceToken = HttpContext.Current.Request.Headers["DeviceToken"];
                        // notify changes only to the same user, excluding the originating device itself.
                        var query = this.NotificationContext.DeviceTokens.Where(o => o.UserId == user.Id);
                        if (!string.IsNullOrEmpty(deviceToken))
                            query = query.Where(o => o.Token != deviceToken);
                        var tokens = query.ToList().Select(o => new PushToken
                        {
                            DeviceToken = o.Token,
                            OperatingSystem = PushServiceManager.GetOperatingSystem(o.OperatingSystem)
                        });
                        Dictionary<string, string> extraData = new Dictionary<string, string>();
                        extraData.Add("Sync", "True");
                        manager.QueueSyncNotification(tokens, extraData);
                    }
                }
            }
        }
        private void BeforeExecuteQueryDelegate(QueryArgs queryArgs)
        {
            if (queryArgs.SynchronizationInfo?.UserId != null)
            {
                string userId = queryArgs.SynchronizationInfo.UserId.ToString();
                IQueryable query = queryArgs.Query;
                if (queryArgs.EntityType == typeof(ToDo))
                    queryArgs.Query = query.OfType<ToDo>().Where(o => o.CreatedBy == userId);
            }
        }
        private User GetUser(string username)
        {
            UserManager<User> manager = new UserManager<User>(new UserStore<User>(new IdentityContext()));
            return manager.FindByName(username);
        }
        #endregion
    }
}

In the controller above, we've added two notable delegates: AfterSaveEntitiesDelegate and BeforeExecuteQueryDelegate. These two methods are added to the controller as they play important roles, in which AfterSaveEntitiesDelegate method is called after saving entities to the database. Here we want to trigger push notification to a specific user to be able to sync data automatically upon any changes made to the database. And in BeforeExecuteQueryDelegate, we've ensured that the user will get his/her ToDos (associated with the user Id) before any query is executed.

Next, we need to modify the clients to be able to receive push notifications properly. Open up AppService.cs inside CrosslightToDo.Core/Infrastructure folder. Use the following code.

Code Block
languagec#
using System.Reflection;
using CrosslightToDo.DomainModels.ToDo;
using CrosslightToDo.ViewModels;
using Intersoft.AppFramework;
using Intersoft.AppFramework.Identity;
using Intersoft.AppFramework.ModelServices;
using Intersoft.AppFramework.PushNotification;
using Intersoft.AppFramework.Services;
using Intersoft.Crosslight;
using Intersoft.Crosslight.Containers;
using Intersoft.Crosslight.Data.EntityModel;
using Intersoft.Crosslight.RestClient;
using Intersoft.Crosslight.Services;
using Intersoft.Crosslight.Services.Auth;
using Intersoft.Crosslight.Services.PushNotification;
using Intersoft.AppFramework.Models;
using Intersoft.Crosslight.Data.ComponentModel;
using System;
using System.Threading.Tasks;
namespace CrosslightToDo.Infrastructure
{
    /// <summary>
    ///     Crosslight's shared application initializer.
    ///     This is the perfect place to register repositories, custom services, and other dependencies via IoC.
    /// </summary>
    /// <seealso cref="Intersoft.Crosslight.ApplicationServiceBase" />
    public sealed class CrosslightAppAppService : ApplicationServiceBase
    {
        #region Constructors
        /// <summary>
        ///     Initializes a new instance of the <see cref="CrosslightAppAppService" /> class.
        /// </summary>
        /// <param name="context">
        ///     The application context that implements <see cref="T:Intersoft.Crosslight.IApplicationContext" />
        /// </param>
        public CrosslightAppAppService(IApplicationContext context)
            : base(context)
        {
            string googleProjectId = "158923337970";
            AppSettings appSettings = new AppSettings();
            appSettings.SingleSignOnAppId = "CrosslightToDo";
            appSettings.WebServerUrl = "http://10.211.55.4:18177/";
            appSettings.BaseAppUrl = appSettings.WebServerUrl;
            appSettings.RestServiceUrl = appSettings.BaseAppUrl + "/data/ToDo";
            appSettings.IdentityServiceUrl = appSettings.BaseAppUrl + "/data/Identity";
            appSettings.PushNotificationServiceUrl = appSettings.BaseAppUrl + "/data/PushNotification";
            appSettings.RequiresInternetConnection = true;
            appSettings.LocalDatabaseName = "todo.db3";
            appSettings.LocalDatabaseLocation = LocalFolderKind.Data;
            appSettings.EnableDataSynchronization = true;
            appSettings.DataSynchronizationMode = DataSynchronizationMode.LoadAll;
            appSettings.EnablePushNotification = true;
            // add new services (extensions)
            ServiceProvider.AddService<IUserService, UserService>();
            ServiceProvider.AddService<IAccountService, WebApiAccountService>();
            ServiceProvider.AddService<IAuthenticationService, AuthenticationService>();
            ServiceProvider.AddService<IPushNotificationService, PushNotificationService>();
            ServiceProvider.AddService<IPushRegistrationService, PushRegistrationService>();
            // shared services registration
            this.GetService<ITypeResolverService>().Register(typeof(CrosslightAppAppService).GetTypeInfo().Assembly);
            // components specific registration
            this.GetService<IActivatorService>().Register<IRestClient>(c =>
            {
                RestClient restClient = new RestClient(appSettings.RestServiceUrl);
                restClient.TypeResolver = new EntityTypeResolver();
                restClient.AuthenticationServiceId = this.AccountService.ServiceId;
                restClient.AuthenticationUrl = appSettings.AuthenticationUrl;
                restClient.Account = this.AccountService.GetAccount();
                return restClient;
            });
            Container.Current.RegisterInstance(appSettings);
            // data sync service
            if (appSettings.EnableDataSynchronization)
            {
                if (appSettings.DataSynchronizationMode == DataSynchronizationMode.LoadAll)
                {
                    Container.Current.Register<IEntityContainer>("Default", c => new EntityContainer()
                    {
                        EnableSynchronization = true
                    }).WithLifetimeManager(new ContainerLifetime());
                }
                else
                {
                    Container.Current.Register<IEntityContainer>("Default", c => new LocalEntityContainer()
                    {
                        EnableSynchronization = true
                    }).WithLifetimeManager(new ContainerLifetime());
                }
            }
            else
                Container.Current.Register<IEntityContainer>("Default", c => new EntityContainer()).WithLifetimeManager(new ContainerLifetime());
            Container.Current.Register<IUserRepository, UserRepository>();
            Container.Current.Register<IPushNotificationRepository, PushNotificationRepository>();
            Container.Current.Register<IToDoRepository>((c) => new ToDoRepository(c.Resolve<IEntityContainer>("Default")));
            // local data storage service
            ServiceProvider.AddService<ISQLiteService, SQLiteService>();
            // push service
            IPushNotificationService pushNotificationService = this.GetService<IPushNotificationService>();
            pushNotificationService.Initialize(new PushNotificationSettings
            {
                GoogleProjectId = googleProjectId
            });
            // data sync service
            ServiceProvider.AddService<ISynchronizationService>((c) =>
            {
                IEntityContainer container = Container.Current.Resolve<IEntityContainer>("Default");
                ISynchronizationService service = new SynchronizationService(container);
                service.SynchronizationTypes = ToDoEntities.SynchronizationTypes;
                // Uncomment the following line to show verbose sync messages, ideal for debugging purpose
                // service.ShowStatus = true;
                return service;
            });
        }
        #endregion
        #region Properties
        public IAccountService AccountService => ServiceProvider.GetService<IAccountService>();
        private bool IsDataSynchronizationEnabled
        {
            get { return Container.Current.Resolve<AppSettings>().EnableDataSynchronization; }
        }
        private ISynchronizationService SynchronizationService
        {
            get { return this.GetService<ISynchronizationService>(); }
        }
        #endregion
        #region Methods
        protected async override void OnDeviceTokenReceived(DeviceToken deviceToken)
        {
            base.OnDeviceTokenReceived(deviceToken);
            var pushRegistrationService = ServiceProvider.GetService<IPushRegistrationService>();
            var userService = this.GetService<IUserService>();
            var user = userService.GetCurrentUser();
            // device token is received from Platform Store Service
            // now check if the token and user has been registered in our app
            if (await pushRegistrationService.ShouldRegisterDeviceTokenAsync(deviceToken))
            {
                await pushRegistrationService.SaveDeviceTokenAsync(deviceToken, user, false);
                await pushRegistrationService.RegisterDeviceTokenAsync(deviceToken, user);
            }
        }
        protected override void OnResume()
        {
            base.OnResume();
            this.OnSync(null);
        }
        /// <summary>
        ///     Called when the application is starting.
        /// </summary>
        /// <param name="parameter">The startup parameters.</param>
        protected override void OnStart(StartParameter parameter)
        {
            base.OnStart(parameter);
            this.AccountService.Initialize(typeof(LoginViewModel));
            //Specify the first ViewModel to use when launching the application.
            this.SetRootViewModel<SimpleViewModel>();
        }
        protected override void OnSync(SyncContext context)
        {
            try
            {
                base.OnSync(context);
                if (this.IsDataSynchronizationEnabled)
                {
                    if (this.SynchronizationService.DefaultQueryDefinition == null)
                    {
                        var service = this.GetService<IUserService>();
                        var task = TaskEx.Run(async () => await service.GetCachedUserAsync());
                        var user = task.WaitForResult();
                        if (user != null)
                            service.SetCurrentUser(user);
                        var queryDefinition = new LocalTypeQueryDefinition();
                        var queryDescriptor = new QueryDescriptor();
                        queryDescriptor.FilterDescriptors.Add(new FilterDescriptor("CreatedBy", FilterOperator.IsEqualTo, user.Id));
                        queryDefinition.AddQuery(typeof(ToDo), queryDescriptor);
                        // set as the default query for synchronization service
                        this.SynchronizationService.DefaultQueryDefinition = queryDefinition;
                    }
                    if (this.AccountService != null && this.AccountService.IsLoggedIn())
                        this.SynchronizationService.SynchronizeDataAsync();
                }
            }
            catch (Exception ex)
            {
                var presenterService = this.GetService<IPresenterService>();
                if (presenterService != null)
                {
                    var toastPresenter = presenterService.GetPresenter<IToastPresenter>();
                    if (toastPresenter != null)
                        toastPresenter.Show(ex.Message);
                }
            }
        }
        #endregion
    }
}

Enabling Push Notifications on iOS

Next, open up Info.plist inside CrosslightToDo.iOS. Add the following snippet to enable push notifications.

Code Block
languagexml
<key>UIBackgroundModes</key>
<array>
	<string>remote-notification</string>
</array>

Then, open AppInitializer.cs inside CrosslightToDo.iOS/Infrastructure folder and add the following line inside the InitializeServices method.

Code Block
languagec#
UIApplicationDelegate.PreserveAssembly((typeof(PushNotification.ServiceInitializer).Assembly));

When you start up the application, you should get the following screen.

Enabling Push Notifications on Android

To enable push notifications on Android, follow these steps. Add a new empty class inside CrosslightToDo.Android/Infrastructure folder. Name the class NotificationBootReceiver and use the following code.

Code Block
languagec#
using Android.App;
using Android.Content;
using Intersoft.AppFramework.Identity;
using Intersoft.Crosslight;
using Intersoft.Crosslight.Services.PushNotification.Android;
using Android.Widget;
namespace CrosslightToDo.Android
{
    /// <summary>
    ///     Represents application google cloud messaging boot receiver.
    /// </summary>
    [BroadcastReceiver]
    [IntentFilter(new[] {Intent.ActionBootCompleted})]
    [IntentFilter(new[] {Intent.ActionUserPresent})]
    public class NotificationBootReceiver : GoogleCloudMessagingBootReceiver
    {
        #region Properties
        /// <summary>
        ///     Gets or sets the notification icon identifier.
        /// </summary>
        /// <value>
        ///     The notification icon identifier.
        /// </value>
        public override int NotificationIconId
        {
            get { return Resource.Drawable.ic_toolbar; }
        }
        #endregion
        #region Methods
        public override void OnReceive(Context context, Intent intent)
        {
            //base.OnReceive(context, intent);
            IAccount account = ServiceProvider.GetService<IAccountService>().GetAccount();
            if (account != null)
            {
                ISyncService syncService = ServiceProvider.GetService<ISyncService>();
                syncService.EnableSync(account);
            }
        }
        #endregion
    }
}

Then, still in the same folder, add another empty class called NotificationBroadcastReceiver and use the following code.

Code Block
languagec#
using Android.App;
using Android.Content;
using Intersoft.Crosslight.Services.PushNotification.Android;
namespace CrosslightToDo.Android
{
    /// <summary>
    ///     Represents application google cloud messaging broadcast receiver.
    /// </summary>
    [BroadcastReceiver(Permission = "com.google.android.c2dm.permission.SEND")]
    [IntentFilter(new[] {"com.google.android.c2dm.intent.RECEIVE"}, Categories = new[] {"@PACKAGE_NAME@"})]
    [IntentFilter(new[] {"com.google.android.c2dm.intent.REGISTRATION"}, Categories = new[] {"@PACKAGE_NAME@"})]
    [IntentFilter(new[] {"com.google.android.gcm.intent.RETRY"}, Categories = new[] {"@PACKAGE_NAME@"})]
    public class NotificationBroadcastReceiver : GoogleCloudMessagingBroadcastReceiver
    {
        #region Properties
        /// <summary>
        ///     Gets or sets the notification icon identifier.
        /// </summary>
        /// <value>
        ///     The notification icon identifier.
        /// </value>
        public override int NotificationIconId
        {
            get { return Resource.Drawable.ic_toolbar; }
        }
        #endregion
    }
}

These two classes are required for the Android app to properly receive push notifications with appropriate permissions. Then, open up AndroidManifest.xml inside CrosslightToDo.Android/Properties folder. Paste the following code.

Code Block
languagexml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="CrosslightToDo.Android" android:installLocation="preferExternal">
	<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21" />
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
	<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
	<application android:label="CrosslightToDo.Android" android:theme="@style/Theme.Crosslight.Material.Light" android:icon="@mipmap/icon"></application>
	<uses-permission android:name="android.permission.INTERNET" />
	<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
	<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
	<uses-permission android:name="android.permission.GET_ACCOUNTS" />
	<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
	<uses-permission android:name="android.permission.USE_CREDENTIALS" />
	<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
	<uses-permission android:name="android.permission.WAKE_LOCK" />
	<uses-permission android:name="android.permission.GET_TASKS" />
	<uses-permission android:name="android.permission.C2D_MESSAGE" />
	<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
	<permission android:name="crosslighttodo.android.permission.C2D_MESSAGE" android:protectionLevel="signature" />
</manifest>

In the above code, we've added several required permissions that are necessary for the OS-level synchronization and push notifications to work properly. Now, let's move on by creating the sync adapters. In CrosslightToDo.Android project, add a new folder called Services, then a folder called Synchronization inside it.

Inside the Synchronization folder, create four empty classes, respectively as follows.

Code Block
titleToDoSyncAdapter.cs
languagec#
using Android.Content;
using Intersoft.Crosslight.Android;
namespace CrosslightToDo.Android
{
    public class ToDoSyncAdapter : BackgroundSyncAdapter
    {
        #region Constructors
        /// <summary>
        ///     Initializes a new instance of the <see cref="BackgroundSyncAdapter" /> class.
        /// </summary>
        /// <param name="context">Context.</param>
        /// <param name="autoInitialize">Auto initialize.</param>
        public ToDoSyncAdapter(Context context, bool autoInitialize)
            : base(context, autoInitialize)
        {
        }
        /// <summary>
        ///     Initializes a new instance of the <see cref="BackgroundSyncAdapter" /> class.
        /// </summary>
        /// <param name="context">Context.</param>
        /// <param name="autoInitialize">Auto initialize.</param>
        /// <param name="allowParallelSyncs">Allow parallel syncs.</param>
        public ToDoSyncAdapter(Context context, bool autoInitialize, bool allowParallelSyncs)
            : base(context, autoInitialize, allowParallelSyncs)
        {
        }
        #endregion
    }
}
Code Block
titleToDoSyncAuthenticatorService.cs
languagec#
using Android.App;
using Intersoft.Crosslight.Android;
namespace CrosslightToDo.Android
{
    [Service(Name = "crosslighttodo.android.ToDoSyncAuthenticatorService")]
    [IntentFilter(new[] {"android.accounts.AccountAuthenticator"})]
    [MetaData("android.accounts.AccountAuthenticator", Resource = "@xml/sync_authenticator")]
    public class ToDoSyncAuthenticatorService : BackgroundSyncAuthenticatorService
    {
    }
} 
Code Block
titleToDoSyncProvider.cs
languagec#
using Android.Content;
using Intersoft.Crosslight.Android;
namespace CrosslightToDo.Android.Services
{
    [ContentProvider(new[] {"crosslighttodo.todosyncprovider"}, Label = "To Do Data", Exported = false, Syncable = true)]
    public class ToDoSyncProvider : BackgroundSyncProvider
    {
    }
}
Code Block
titleToDoSyncService.cs
languagec#
using Android.App;
using Intersoft.Crosslight.Android;
namespace CrosslightToDo.Android
{
    [Service(Name = "crosslighttodo.android.ToDoSyncService", Exported = true)]
    [IntentFilter(new[] {"android.content.SyncAdapter"})]
    [MetaData("android.content.SyncAdapter", Resource = "@xml/sync_adapter")]
    public class ToDoSyncService : BackgroundSyncService
    {
        #region Methods
        protected override BackgroundSyncAdapter InitializeSyncAdapter()
        {
            return new ToDoSyncAdapter(ApplicationContext, true);
        }
        #endregion
    }
}

These are all the main components of creating a usable sync services on Android.

Next, open up the CrosslightToDo.Android/Resources folder. Create a new folder inside called xml. Then, create two new XML files inside it, respectively.

Code Block
titlesync_adapter.xml
languagexml
<?xml version="1.0" encoding="UTF-8" ?>
<sync-adapter
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:contentAuthority="crosslighttodo.todosyncprovider"
        android:accountType="WebApi_OSSyncApp"
        android:userVisible="true"
        android:supportsUploading="true"
        android:allowParallelSyncs="false"
        android:isAlwaysSyncable="true"/>
Code Block
titlesync_authenticator.xml
languagexml
<?xml version="1.0" encoding="UTF-8" ?>
<account-authenticator
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:accountType="WebApi_OSSyncApp"
        android:icon="@drawable/ic_toolbar"
        android:smallIcon="@drawable/ic_drawer"
        android:label="@string/ApplicationName"/>

Once that's done, run the Android project on a real device. To test the iOS version, you'll need to deploy it on a device. You cannot use push notifications feature when running on an iOS simulator. You should get the following result. Here's an example running the sample on an Android device an a iOS simulator.

Video
Autoplayfalse
Sourcehttp://developer.intersoftsolutions.com/download/attachments/27297667/sync-push.mp4?api=v2
Width500px

Conclusion

Should you be able to run both of the simulators side by side, you can observe the following interaction.

In the next tutorial, we're going to further enhance this sample with multiple sync channels.

Sample

You can also find the resulting sample here: CrosslightToDoOSSync.zip. Simply open this sample and restore the NuGet packages with Xamarin Studio or Visual Studio and run the project. Consult this documentation should you need help on restoring the NuGet packages.

 

Related Topics

Content by Label
spacescrosslight
reversetrue
showLabelsfalse
max5
sortmodified
labelsgetting-started-with-crosslight -walkthrough-create-a-to-do-app-with-os-level-data-synchronization
showSpacefalse
typepage