Xamarin.Native Tutorial: InvoiceJe: DataBinding: iOS

Xamarin.Native Tutorial: InvoiceJe: DataBinding: iOS

(Click here to view the table of contents of this Xamarin.Native tutorial)

Now let's go with iOS.

Our tutorial will heavily refer https://developer.xamarin.com/guides/ios/user_interface/controls/tables/

Open Main.storyboard.

Open the Invoices TableViewController.

If you've followed from the previous tutorial, there wouldn't be any UITableView inside it yet. So drag a UITableView onto the Invoices TableViewController.

Now, click on a row inside the UITableView. Change the Style to Basic (This one doesn't really matter, but just do it). Change the Accessory to Disclosure Indicator. Change the Identifier to TaskCell.

If you haven't, rebuild the iOS project.

Now click on the Invoices TableViewController. Click Properties. Change the Identity>Class to InvoicesTableViewController. This will automatically create a InvoicesTableViewController.cs class in the root of the iOS project.

Now, we've set InvoicesTableViewController as the controller. Now we can freely modify things inside it.

 Now, open InvoicesTableViewController.cs and replace it with this:

using Foundation;
using InvoiceJe.Data;
using InvoiceJe.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using UIKit;

namespace InvoiceJe.iOS
{
    public partial class InvoicesTableViewController : UITableViewController
    {
        public InvoicesTableViewController (IntPtr handle) : base (handle)
        {
        }

        public override void ViewWillAppear(bool animated)
        {
            base.ViewWillAppear(animated);

            var repository = new Repository();
            TableView.Source = new InvoicesTableSource(repository.GetInvoices());
        }
    }


    public class InvoicesTableSource : UITableViewSource
    {

        private IEnumerable<Invoice> _invoices;
        private string _cellIdentifier = "TaskCell";

        public InvoicesTableSource(IEnumerable<Invoice> invoices)
        {
            _invoices = invoices;
        }

        public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
        {
            UITableViewCell cell = tableView.DequeueReusableCell(_cellIdentifier);

            // If there are no cells to reuse, create a new one
            if (cell == null)
            {
                cell = new UITableViewCell(UITableViewCellStyle.Default, _cellIdentifier);
            }

            Invoice invoice = _invoices.ElementAt(indexPath.Row);
            cell.TextLabel.Text = invoice.ReferenceNumber;

            return cell;
        }

        public override nint RowsInSection(UITableView tableview, nint section)
        {
            return _invoices.Count();
        }
    }

}

...Now try running the app. Your table should already be populated by the repository.

Explanation

Now, to populate a TableView with dynamic data, what you need is to have a UIViewController that implements IUITableViewDataSource (to handle data population), IUITableViewDelegate (to handle events).

Fortunately, we have a UITableViewController that handles both of that for us. So, we can simply inherit InvoicesTableViewController from UITableViewController.

Next is, we need to tell our TableView what is its DataSource. To do that, we create a new class, names InvoicesDataSource, that inherits from UITableViewSource.

Let's take a look at the private properties and constructor:

        private IEnumerable<Invoice> _invoices;
        private string _cellIdentifier = "TaskCell";

        public InvoicesTableSource(IEnumerable<Invoice> invoices)
        {
            _invoices = invoices;
        }

..We simply send the Invoices into our InvoicesTableSource and defined the _cellIdentifier.

        public override nint RowsInSection(UITableView tableview, nint section)
        {
            return _invoices.Count();
        }

...Here, we tell iOS how many rows are there inside a section inside our table. Since we only have 1 section, we can simply return the total number of invoices via _invoices.Count().

Next will be the GetCell() implementation:

        public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
        {
            UITableViewCell cell = tableView.DequeueReusableCell(_cellIdentifier);

            // If there are no cells to reuse, create a new one
            if (cell == null)
            {
                cell = new UITableViewCell(UITableViewCellStyle.Default, _cellIdentifier);
            }

            Invoice invoice = _invoices.ElementAt(indexPath.Row);
            cell.TextLabel.Text = invoice.ReferenceNumber;
            return cell;
        }

The lines..

            UITableViewCell cell = tableView.DequeueReusableCell(_cellIdentifier);

            // If there are no cells to reuse, create a new one
            if (cell == null)
            {
                cell = new UITableViewCell(UITableViewCellStyle.Default, _cellIdentifier);
            }

...is required for caching. Basically what happens behind the scenes is that, if you have a table with 1000 rows, rather than iOS rendering 1000s of rows offscreen, it simply reuse the cells you see onscreen by changing the data inside each row based on how far you scroll. The _cellIdentifier allows iOS to identify whether it's re-using the correct cell template, which in our case, defined as "TaskCell". More information can be gleaned from this stackoverflow question.

And also, we've set the UITableViewCell to use the Default style. There are plenty other styles you can experiment with, or you can even define a custom one yourself.

            Invoice invoice = _invoices.ElementAt(indexPath.Row);
            cell.TextLabel.Text = invoice.ReferenceNumber;
            return cell;

...and lastly, these lines simply gets the Invoice, replace the Text with the ReferenceNumber and then returns the cell. Easy-peasy, right?

Here's how your app should look like:

Cool.

Edit Invoices

Now, let's create an Edit Invoices page.

At Main.storyboard, open Toolbox and drag a TableViewController onto the storyboard. Let's name it InvoicesEditViewController.

Ctrl+drag from the TableView inside InvoicesTableViewController to the InvoicesEditViewController to create a segue between them. This means each time a row in TableView is clicked, the screen will navigate to InvoicesEditViewController.

Now let's make a sweet-looking form. Now, there are many ways to design a form, but I will show you one way to make a nice looking form using TableView.

You should make it look like this:

Refer to the previous tutorial to make a TableView like this, but I'll list down the steps here:

Click on the TableView. Change the content to Static Cells and Section 1. Style is Grouped. Separator is Single Line. 

Open Toolbox. Drag a TextField into one of the row.

Resize it to fill the row vertically, and have a margin left of 16 and margin right of 16 (If you resize with mouse, the iOS will automatically snap it for you).

Set the constraints so that the height is equal to the superview (in this case, it's the row). Double-click on the TextField to change into the Constraint mode, drag the "I" on the right-most side to the right-edge of the row to tell iOS "This TextField needs to be the same size as this row".

Set the constraints for the left and right margins too. This time, drag the left 'T' to the left edge of the row, and then drag the right 'T' to the right side of the row.

Click on Properties, change to the Layout tab and look at the Constraints section. It should look like this:

...Lastly, click on the TextField. Change the Border Style to empty. Remove the Text property and set the Name to "ReferenceNumber" (this will be the ID when we refer to this TextField from our code-behind) and Placeholder to "Reference Number".

If there are other rows in the TableView, delete them.

Then, select the row with your shining new TextField and copy. And then paste 2 times and VS will add 2 new rows for you.

Change the PlaceholderTexts for the remaining rows' Name and Placeholders to "BillTo" and "Bill To" and "Amount" and "Amount" respectively.

Done!

Now we're going to add a Save button at the top-right of the screen.

However, if you drag a Button to the top bar, it will only be added to the TableView.

You can't, because technically there's no UINavigationItem inside the InvoicesEditTableViewController.

So, open Toolbox and drag UINavigationItem to the top of the scene. Now drag a button to the Top Bar. Change the Title to 'Save'. Awesome!

Sending From the Table to the Correct Invoice and also Actually Populating the InvoicesEdit screen

Yeah, I'm totally a fan of long titles.

Btw, here's what we're going to do:

  1. Send the InvoiceId from the InvoicesViewController to InvoicesEditViewController
  2. Actually populating the InvoicesEdit screen with the correct Invoice

Let's go!

Before that, let's create the InvoicesEditTableViewController first.

So, open Main.storyboard. Click on the InvoicesEditTableViewController we've made. Change the Identity -> Class to InvoicesEditTableViewController. VS will automatically create a InvoicesEditTableViewController class in the root of the iOS project.

Now this is where it's a bit different than Android.

Rather than creating an Intent and filling it with parameters for the next Activity to consume then simply start the activity, we override the function PrepareForSegue inside the source ViewController (in this case, InvoicesTableViewController). Inside the PrepareForSegue function, we can directly manipulate the Public Properties of the destination ViewController (in this case, InvoicesEditViewController).

Firstly, open InvoicesEditTableViewController.cs.

  1. Since we need to receive InvoiceId from the previous ViewControoler, we're going to add a Public property Int InvoiceId.
  2. Next, we're going to update the TextFields with information from our Invoice

Hence, this is how InvoicesEditTableViewController.cs should look like (simply copy paste):

using Foundation;
using InvoiceJe.Data;
using System;
using System.Linq;
using UIKit;

namespace InvoiceJe.iOS
{
    public partial class InvoicesEditTableViewController : UITableViewController
    {

        public int InvoiceId { get; set; }

        public InvoicesEditTableViewController (IntPtr handle) : base (handle)
        {
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            var repository = new Repository();
            var invoice = repository.GetInvoices().Where(x => x.Id == InvoiceId).FirstOrDefault();

            ReferenceNumber.Text = invoice.ReferenceNumber;
            BillTo.Text = invoice.BillTo;
            Amount.Text = invoice.Amount.ToString();

        }
    }
}

Alright, now let's open Main.storyboard. Locate the Segue that goes from InvoicesTableViewController to InvoicesEditViewController. Click on it and open Properties. Now set the Identifier property to "ShowInvoicesEditViewController".

Now, open InvoicesTableViewController.cs. What's going to happen is:

  1. We override the PrepareForSegue() method
  2. This is funny, but iOS couldn't automatically differentiate between segues if there are 2 or more segues that originate from a ViewController. This is where the Identifier comes in. We need to tell iOS that "if Segue's Identifier is 'xxx', THEN do something like this, if not then do something else".
  3. Lastly, we simply set the Public Properties of the target ViewController, which is InvoicesEditViewController.

So, simply copy paste the following code:

using Foundation;
using InvoiceJe.Data;
using InvoiceJe.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using UIKit;

namespace InvoiceJe.iOS
{
    public partial class InvoicesTableViewController : UITableViewController
    {

        private IEnumerable<Invoice> _invoices;

        public InvoicesTableViewController (IntPtr handle) : base (handle)
        {
            var repository = new Repository();
            _invoices = repository.GetInvoices();
        }

        /// <summary>
        /// Happens every time a view loads
        /// </summary>
        /// <param name="animated"></param>
        public override void ViewWillAppear(bool animated)
        {
            base.ViewWillAppear(animated);
            TableView.Source = new InvoicesTableSource(_invoices);
        }

        public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
        {            
            base.PrepareForSegue(segue, sender);

            if (segue.Identifier == "ShowInvoicesEditViewController")
            {
                var editViewController = segue.DestinationViewController as InvoicesEditTableViewController;
                var row = TableView.IndexPathForSelectedRow;
                var invoice = _invoices.ElementAt(row.Row);
                editViewController.InvoiceId = invoice.Id;
            }
        }
    }


    public class InvoicesTableSource : UITableViewSource
    {

        private IEnumerable<Invoice> _invoices;
        private string _cellIdentifier = "TaskCell";

        public InvoicesTableSource(IEnumerable<Invoice> invoices)
        {
            _invoices = invoices;
        }

        public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
        {
            UITableViewCell cell = tableView.DequeueReusableCell(_cellIdentifier);

            // If there are no cells to reuse, create a new one
            if (cell == null)
            {
                cell = new UITableViewCell(UITableViewCellStyle.Default, _cellIdentifier);
            }

            Invoice invoice = _invoices.ElementAt(indexPath.Row);
            cell.TextLabel.Text = invoice.ReferenceNumber;
            return cell;
        }

        public override nint RowsInSection(UITableView tableview, nint section)
        {
            return _invoices.Count();
        }

    }

}

...Now try playing it. Your iOS app should now look pretty awesome!

Explanation:

            if (segue.Identifier == "ShowInvoicesEditViewController")
            {
                var editViewController = segue.DestinationViewController as InvoicesEditTableViewController;
                var row = TableView.IndexPathForSelectedRow;
                var invoice = _invoices.ElementAt(row.Row);
                editViewController.InvoiceId = invoice.Id;
            }

The segue.Identifier line is so that we only run the code if the specific "ShowInvoicesEditViewController" is triggered.

We can directly modify the destination ViewController by accessing segue.DestinationViewController.

We can get the selected row inside the TableView by accessing TableView.IndexPathForSelectedRow.

Then, from the number of selected row, we get the exact invoice.

Lastly, we simply set the InvoiceId property of the InvoicesEditViewController.

Alright, now we've did it, remember the InvoicesCreateViewController that we never got around before? Let's do it.

You know what, I'm not gonna hand-hold you through it. Basically you just create the same thing you did for the EditInvoices we just did, okay? ;)

You've done that? AWESOME! Now let's actually connect with a database!

 Sample at Github: https://github.com/purrmiaw/InvoiceJeBindingNoDatabase