Chapter 5. User Interface

Here is a quick look at what we'll cover in this chapter:

  • How to leverage platform-specific APIs to extend the default behavior of Xamarin.Forms controls with Custom Renderers
  • How to manipulate the visual appearance of bound data with Value Converters

Custom Renderers

One of the paramount features of the Xamarin.Forms toolkit is the layer of abstraction it provides over user interface implementation. With a single API, Xamarin.Forms allows you to use native user interface controls and functionality. For example, the Entry class at runtime will display a UITextField view on iOS, an EditText widget on Android, and a TextBox control on Windows. The toolkit does this by using a concept called Renderers. The renderers correspond with the visual elements—controls, pages, and layouts—within the API. So, for example, there is an EntryRenderer that is responsible for rendering instances of the Entry class down to the platform-specific versions of that control.

The beauty of this renderer concept is that you can subclass the various renderer classes to override how a specific element is translated at runtime. So, for example, if you wanted all textboxes in your app (that is, every time you display an Entry element) to be completely borderless, you could simply write a new EntryRenderer sub-class for each platform that removes the border on the platform-specific element. However, you typically won't want to completely override the default controls of the toolkit. The more common solution is to create a custom control by sub-classing a Xamarin.Forms element and then writing the renderer specifically for that custom class. So then, instead of removing the border from all uses of Entry, you would instead use a custom Entry class, for example NoBorderEntry, that, when rendered, will be borderless.

The concept of Custom Renderers is a very powerful and handy utility when building rich apps using the Xamarin.Forms toolkit. Using the default controls and behaviors of the toolkit will certainly render a native experience, but they can limit you in more complex scenarios. Custom Renderers will ensure that you can exceed these limits when needed to deliver the exact experience you want.

Creating a TableView DatePicker

In our TripLog app, we are using a TableView with EntryCells to present a form to the user to add a new log entry. Currently, the date field in the form is using a regular EntryCell that presents an editable text field with the default keyboard. Obviously, this is not an ideal user experience, and it also causes a nightmare when it comes to data validity. Ideally, when the user taps into this date field, they should be presented with a standard, platform-specific date picker.

The Xamarin.Forms API provides the DatePicker control; however, it is based on a View, not a ViewCell. The only way to use the DatePicker control in a TableView would be to wrap it in a ViewCell, as follows:

var datePickerCell = new ViewCell {
    View = new DatePicker()
};

While this approach will work, it is somewhat limited. It is simply a control embedded in a ViewCell; it does not have the same look and feel as the rest of the cells in the TableView. In order to get a similar look and feel to the EntryCells used in the TableView, you would have to add a label and also mess with the margins, spacing, and sizing to get it to look just right. Another minor downside to this approach is that, in order to capture both date and time, you will need to include two separate cells—one that includes DatePicker, and one that includes TimePicker. The iOS UIDatePicker actually provides a mode that lets the user pick both the date and time in the same picker. Android does not have this same capability; but if we're going to make a Custom Renderer, we can at least take advantage of the dual mode on iOS.

So, in order to overcome these limitations and deliver the best experience possible, we can create a Custom Renderer that extends the EntryCellRenderer to display an EntryCell that behaves like the standard DatePicker control.

Because we don't want to render all EntryCells in our application with date picker functionality, the first thing we need to do is a create a custom EntryCell control that the Custom Renderer will be affiliated with. We can create this in a Controls folder within the core library of our TripLog app, as follows:

  1. First, create a new folder in the core project named Controls and, within it, create a new class named DatePickerEntryCell that inherits from EntryCell:
    public class DatePickerEntryCell : EntryCell
    {
    }
  2. Next, add a DateTime BindableProperty so that this custom control can be data bound just like any other control:
    public class DatePickerEntryCell : EntryCell
    {
        public static readonly BindableProperty DateProperty =
           BindableProperty
              .Create<DatePickerEntryCell, DateTime>(p =>
                 p.Date,
                 DateTime.Now,
                 propertyChanged: new BindableProperty
                    .BindingPropertyChangedDelegate<DateTime>(
                       DatePropertyChanged));
    
        public DateTime Date
        {
            get { return (DateTime)GetValue(DateProperty); }
            set { SetValue(DateProperty, value); }
        }
    
        public new event EventHandler Completed;
    
        static void DatePropertyChanged(BindableObject bindable,
         DateTime oldValue,
         DateTime newValue)
        {
            var @this = (DatePickerEntryCell)bindable;
    
            if (@this.Completed != null)
                @this.Completed (bindable, new EventArgs ());
        }
    }

Notice how the Completed EventHandler is used in conjunction with the DateProperty PropertyChanged event so we can respond to the Completed events on our DatePickerEntryCell just like we can with a standard EntryCell.

Next, we need to create a custom EntryCellRenderer that will provide the platform-specific functionality for the DatePickerEntryCell, as follows:

  1. Create a new folder in the TripLog.iOS project named Renderers and, within it, create a new class named DatePickerEntryCellRenderer that inherits EntryCellRenderer, as follows:
    public class DatePickerEntryCellRenderer
       : EntryCellRenderer
    {
        // ...
    }
  2. Next, override the EntryCellRenderer GetCell method to override the default EntryCell behavior for iOS by setting InputView of UITextField to a UIDatePicker instance:
    public class DatePickerEntryCellRenderer
       : EntryCellRenderer
    {
        public override UITableViewCell GetCell (Cell item,
           UITableViewCell reusableCell, UITableView tv)
        {
            var cell = base.GetCell (item, reusableCell, tv);
    
            var datepickerCell = (DatePickerEntryCell)item;
    
            UITextField textField = null;
    
            if (cell != null)
               textField = (UITextField)cell.ContentView
                             .Subviews [0];
    
            // Default datepicker display attributes
            var mode = UIDatePickerMode.Date;
            var displayFormat = "d";
            var date = NSDate.Now;
            var isLocalTime = false;
    
            // Update datepicker based on Cell's properties
            if (datepickerCell != null) {
                // Kind must be Universal or Local to cast to
                // NSDate
                if (datepickerCell.Date.Kind == DateTimeKind.Unspecified) {
                 var local = new DateTime (datepickerCell.Date.Ticks, DateTimeKind.Local);
                 date = (NSDate)local;
             }
             else
                date = (NSDate)datepickerCell.Date;
    
             isLocalTime = datepickerCell.Date.Kind == DateTimeKind.Local || datepickerCell.Date.Kind == DateTimeKind.Unspecified;
           }
    
           // Create iOS datepicker
           var datepicker = new UIDatePicker {
              Mode = mode,
              BackgroundColor = UIColor.White,
              Date = date,
              TimeZone = isLocalTime ? NSTimeZone.LocalTimeZone
                                     : new NSTimeZone("UTC")
            };
    
            // Create a toolbar with a done button that will
            // close the datepicker and set the selected value
            var done = new UIBarButtonItem("Done", UIBarButtonItemStyle.Done, (s, e) => {
                var pickedDate = (DateTime)datepicker.Date;
    
                if (isLocalTime)
                    pickedDate = pickedDate.ToLocalTime();
    
                // Update the Date property on the Cell
                if (datepickerCell != null)
                    datepickerCell.Date = pickedDate;
    
                // Update the value of the UITextField within the
                // Cell
                if (textField != null)
                {
                    textField.Text = pickedDate.ToString(displayFormat);
                    textField.ResignFirstResponder();
                }
            });
    
            var toolbar = new UIToolbar {
               BarStyle = UIBarStyle.Default,
               Translucent = false
            };
    
            toolbar.SizeToFit ();
            toolbar.SetItems (new[] { done }, true);
    
            // Set the input view, toolbar and initial value for
            // the Cell's UITextField
            if (textField != null) {
                textField.InputView = datepicker;
                textField.InputAccessoryView = toolbar;
    
                if (datepickerCell != null)
                    textField.Text = datepickerCell.Date.ToString (displayFormat);
            }
    
            return cell;
        }
    }
    
  3. Next, in order to register the Custom Renderer, simply add an ExportRenderer assembly attribute to the class above the namespace declaration. This attribute is required by Xamarin.Forms in order for the Custom Renderer to take action on the control at runtime:
    [assembly: ExportRenderer(typeof(DatePickerEntryCell), typeof(DatePickerEntryCellRenderer))]
  4. Finally, we need to update our NewEntryPage to use our new custom DatePickerEntryCell. Simply update the current date object from EntryCell to DatePickerEntryCell and update the binding to use DateProperty:
    var date = new DatePickerEntryCell {
        Label = "Date"
    };
    date.SetBinding (DatePickerEntryCell.DateProperty,
       "Date",
       BindingMode.TwoWay);

Now, if we run the TripLog app by navigating to the new entry page and tapping into the date field, we will see a native date picker as shown in the next image. As we pick different values in the picker, the DateProperty binding we created will automatically update the ViewModel as well.

Creating a TableView DatePicker

Note

The Android version of this Custom Renderer is available in the companion source code for this book.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.72.74