Single Row GridView using MvvmCross, Infragistics NucliOS, and Xamarin.iOS

MvvmCross is a fantastic framework create by Stuart Lodge. It’s my go-to framework for cross platform development, and works great for any app, regardless of size. Here is a link to the projects GitHub page. Stuart has an excellent series (N+1) on his YouTube Channel as well.

The following example will demonstrate how to create a Core portable project that contains our Models, and ViewModels, as well as an iOS project that uses an Infragistics Grid. MvvmCross is extremely flexible, and allows us to create and register custom bindings, so we can create our own ‘magic’. The source code for this example can be found on GitHub.

Step 1.) Create a new C# PCL Project. We’ll be displaying a collection of Monkey Images, so we’ll name our solution MonkeysList. Since this is the Core project, we’ll call it MonkeysList.Core.

Create new PCL Project

Step 2.) Choose your target platforms for the PCL project. We’ll chose to target Windows Phone 7.5 and higher, Mono for Android, and Monotouch.

2

Step 3.)  Add MvvmCross References. One of the easiest ways to do this is by using Nuget. Search for MvvmCross, and select MvvmCross (It should be the first one in the list).

3

Step 4.) Create Model. Adding the Nuget Packages generates some free code for us! The first thing we’ll do is delete the class.cs file that was created when the PCL project was first created. Next create a folder named Models, and a class Named Monkey, that has a single string type property called ImageUrl.

public class Monkey
{
    public string ImageUrl { get; set; }
}

Step 5.) Create a service that generates a Monkey. This is trivial. First, we need to create a folder called Services. The first that goes in this folder is an Interface, with a single method that creates a Monkey. Next, we need to create a class that Implements this implements this interface.

public interface IMonkeyGenerator
{
     Monkey GenerateMonkey();
}
public class MonkeyGeneratorService : IMonkeyGenerator
{
   public Monkey GenerateMonkey()
   {
     return new Monkey {ImageUrl = string.Format("http://placemonkey.heroku.com/{0}/{0}", Random(20) + 500)};
   }private readonly Random _random = new Random();
 protected int Random(int count)
 {
   return _random.Next(count);
 }
}

Step 6.) Add Monkeys to ViewModel. One of the items created when installing the Nuget packages is FirstViewModel. This comes with an example property. We’ll delete the example, and new list of Monkey. Next, we’ll use MvvmCross to resolve our MonkeyGeneratorService, and use it to create our list of Monkeys. This is done in the class constructor.

public class FirstViewModel 
 : MvxViewModel
 {
 public FirstViewModel()
 {
  //Resolve IMonkeyGenerator from Ioc
  var svc = Mvx.Resolve<IMonkeyGenerator>();
  var monkeys = new List<Monkey>();
 for (int i = 0; i < 20; i++)
 {
   //Generate Monkeys!
   monkeys.Add(svc.GenerateMonkey());
 }
  Monkeys = monkeys;
}
 private List<Monkey> _monkeys;
 public List<Monkey> Monkeys
 {
  get { return _monkeys; }
  set
  {
   _monkeys = value;
   RaisePropertyChanged(() => Monkeys);
  }
 } 
}

That’s it for the PCL project! Below is a picture of what all you should have so far.

4

Step 7.) Create a new Empty iOS project named MonkeysList.Touch
Step 8.) Use Nuget to add MvvmCross to iOS project.
Step 9.) Add a reference to your Core project.
Step 9.) Follow the instructions in ToDo-MvvmCross folder, and replace AppDelegate.cs with the following Code.

 [Register ("AppDelegate")]
 public partial class AppDelegate : MvxApplicationDelegate
 {
   UIWindow _window;
   public override bool FinishedLaunching(UIApplication app, NSDictionary options)
  {
     _window = new UIWindow(UIScreen.MainScreen.Bounds);
     var setup = new Setup(this, _window);
     setup.Initialize();
     var startup = Mvx.Resolve<IMvxAppStart>();
     startup.Start();
    _window.MakeKeyAndVisible();
    return true;
  }
}

Step 11.) Create GridView using Infragistics.

[Register("SingleRowGridView")]
 public class SingleRowGridView : UIView
 {
 //GridView that is displayed
 public IGGridView GridView;
//Column Definition of GridView's column
 private IGGridViewImageColumnDefinition _col;
//Datasource Helper to assist with databinding
 public IGGridViewSingleRowSingleFieldDataSourceHelper Ds;
public SingleRowGridView()
 {
 Initialize();
 }
public SingleRowGridView(RectangleF bounds)
 : base(bounds)
 {
 Initialize();
 }
void Initialize()
 {
BackgroundColor = UIColor.Black;
//Create a Gridview that's a bit farther down than center
 //This Grid has a single row of 200x200 cells
 GridView = new IGGridView(new RectangleF(0f, Frame.Size.Height / 2 - 100, Frame.Size.Width, 200f), IGGridViewStyle.IGGridViewStyleDefault)
 {
 AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleBottomMargin | UIViewAutoresizing.FlexibleTopMargin,
 SelectionType = IGGridViewSelectionType.IGGridViewSelectionTypeCell,
 HeaderHeight = 0f,
 EmptyRows = false,
 RowSeparatorHeight = 0f,
 AllowHorizontalBounce = true,
 AlwaysBounceVertical = false,
 RowHeight = 200f,
 ColumnWidth = IGColumnWidth.CreateNumericColumnWidth(200f),
 Theme = new IGGridViewLightTheme()
 };
AddSubview(GridView);
  //Create Image Column definition for Image property that gets the image from a URL
  _col = new IGGridViewImageColumnDefinition(@"Image",
 IGGridViewImageColumnDefinitionPropertyType
 .IGGridViewImageColumnDefinitionPropertyTypeStringUrl);
   //Create a new Datasource helper that is initialized with the column we just created
   Ds = new IGGridViewSingleRowSingleFieldDataSourceHelper(_col);

   //set the Grid's datasource to the one that we created
   GridView.DataSource = Ds;
   GridView.ReloadData();
  }
}

Step 12.) To bind data to an Infragistics datasource helper, the binding class must inherit from NSObject. This is one of the reasons that we have to implement a custom binding solution in MvvmCross.

//The DataSource helper's data property must be of type NSObject
 [Register("SingleRowData")]
 public class SingleRowData : NSObject
 {
    public SingleRowData(string image)
    {
      Image = image;
    }
   [Export("Image")]
    public string Image { get; set; }
 }

Step 13.) Create Target Binding for our GridView. First, create a new folder named Target. Add a class named SingleRowDataSourceTargetBinding. This class will Inherit from MvxTargetBinding, and override SetValue, TargetType, and DefaultMode. SetValue gets called when the Property Change notification is fired, and is where

//Inherit from MvxTarget binding to perform custom binding 
 public class SingleRowDataSourceTargetBinding : MvxTargetBinding
 {
 //Return View as User Defined View
 protected SingleRowGridView View
 {
     get { return Target as SingleRowGridView; }
 }
public SingleRowDataSourceTargetBinding(SingleRowGridView view)
 : base(view)
 {
    if (view == null)
    {
      MvxBindingTrace.Trace(MvxTraceLevel.Error, "Error - GridViewSource is null in SingRowDataSourceTargetBinding");
    }
 }
//Override SetValue to perform binding
 public override void SetValue(object value)
 {
   var view = View;
   if (view == null)
     return;

   var monkeys = (List<Monkey>)value;   //Grab items from list that was passed in, and create new NSObject[]
 if (monkeys != null)
 {
    View.Ds.Data = monkeys.Select(t => new SingleRowData(t.ImageUrl)).ToArray();
  //Must Call ReloadData 
    View.GridView.ReloadData();
    }
  }
//Set the target type
 public override Type TargetType
 {
    get { return typeof(NSObject[]); }
 }
//Set the Default binding Mode
 public override MvxBindingMode DefaultMode
 {
    get { return MvxBindingMode.OneWay; }
 }
}

Step 14.) Register the Target Binding just created. In the Setup.cs class that was generated from adding MvvmCross from Nuget, override the FillTargetFactories method, and register the target binding.

//Set up Target Bindings
 protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
 {
    registry.RegisterCustomBindingFactory<SingleRowGridView>("SingleRowData", source => new SingleRowDataSourceTargetBinding(source));
    base.FillTargetFactories(registry);
 }

Step 15.) Edit FirstView, set up bindings, and profit! Delete the code that was generated by MvvmCross for binding to the example HelloWorld string. Add a new SingleRowGridView to FirstView, and create the bindings.

[Register("FirstView")]
 public class FirstView : MvxViewController
 {
    private SingleRowGridView _gridView;
   public override void ViewDidLoad()
   {

     base.ViewDidLoad();
    //Initialize new SingleRowGridView
    _gridView = new SingleRowGridView(View.Bounds);
   // ios7 layout
   if (RespondsToSelector(new Selector("edgesForExtendedLayout")))
       EdgesForExtendedLayout = UIRectEdge.None;

   //Bind GridView 
   var set = this.CreateBindingSet<FirstView, Core.ViewModels.FirstViewModel>();
   set.Bind(_gridView).For("SingleRowData").To(vm => vm.Monkeys);
   set.Apply();
  Add(_gridView);
 }
}

Here’s what the end result looks like.

iOS Simulator Screen shot Oct 14, 2013 11.26.48 PM

Thanks for stopping by! Enjoy!

Advertisements

Xamarin.iOS Stopwatch control using Infragistics Nuclios

At work I have the pleasure of using some awesome Infragistic controls for winform applications. At local .Net users group meeting I won a license for the NetAdvantage Ultimate Suite! Awesomesauce! Included with this suite is a set of controls for iOS. They’ve even compiled dll’s for monotouch! Included in their sample projects is an awesome Stopwatch that uses a couple of IGGaugeViews. All of the samples shipped with nucliOS are all in objective-c. With a bit of help from my friends at Infragistics, I was able to get the Stopwatch control Xamafied. The sample code can be downloaded on Github . You’ll need to install nuclios. A free trial can be found here . Below is the Stopwatch UIView…

    public class StopWatch : UIView
    {
        private IGGaugeView _backgroundGauge;
        private UIImageView _glassImage;
        public IGGaugeView HoursGauge;
        private UIImageView _imageView;

        public StopWatch()
        {
            Setup();
        }

        public string ImageName
        {
            get { return @"Clock2.png"; }
        }

        public override RectangleF Frame
        {
            get { return base.Frame; }
            set
            {
                base.Frame = value;

                if (_backgroundGauge == null)
                    return;

                float val = .11f;

                if (Frame.Size.Height > Frame.Size.Width)
                {
                    val = .09f;
                }

                if (UIScreen.MainScreen.PreferredMode.Size == new SizeF(640, 1136))
                {
                    val = .070f;
                }

                _backgroundGauge.CenterY = HoursGauge.CenterY = .48f + val;
            }
        }

        public void Setup()
        {
            if (HoursGauge == null)
            {
                _imageView = new UIImageView(new RectangleF(0, 0, Frame.Size.Width, Frame.Size.Height))
                {
                    AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight,
                    ContentMode = UIViewContentMode.ScaleAspectFit,
                    Image = UIImage.FromBundle(ImageName)
                };
                AddSubview(_imageView);

                // Initialize all Gauges with the same exact frame
                HoursGauge = new IGGaugeView
                {
                    Frame = new RectangleF(0, 0, Frame.Size.Width, Frame.Size.Height),
                    AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight
                };
                _backgroundGauge = new IGGaugeView
                {
                    Frame = new RectangleF(0, 0, Frame.Size.Width, Frame.Size.Height),
                    AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight
                };

                // Add all of our subviews to the visual tree
                AddSubview(_backgroundGauge);
                AddSubview(HoursGauge);

                // Hide the 0 label
                _backgroundGauge.DuplicateLabelOmissionStrategy =
                    IGGaugeDuplicateLabelOmissionStrategy.IGGaugeDuplicateLabelOmissionStrategyOmitFirst;

                // Hide the other gauge labels
                HoursGauge.Font = UIFont.SystemFontOfSize(1);

                _backgroundGauge.NeedlePivotShape = IGGaugePivotShape.IGGaugePivotShapeNone;

                // Hide elements from our top level gauges by setting the brush color to clear
                _backgroundGauge.NeedleOutline =
                    HoursGauge.MinorTickBrush =
                    HoursGauge.TickBrush =
                    _backgroundGauge.ScaleBrush =
                    HoursGauge.BackingBrush =
                    _backgroundGauge.NeedleBrush =
                    HoursGauge.ScaleBrush = HoursGauge.BackingOutline = new IGBrush(UIColor.Clear);

                HoursGauge.BackingStrokeThickness = 0;

                // Set the Start and End Angles of the scale - All gauges must have the same values
                _backgroundGauge.ScaleStartAngle = HoursGauge.ScaleStartAngle = 270;
                _backgroundGauge.ScaleEndAngle = HoursGauge.ScaleEndAngle = 270 + 360;

                // Since we don't want to see the major tick marks, lets set the internval to a larger number to limit how many tick marks try to render.
                HoursGauge.Interval = 60;

                // Set the Max Values
                _backgroundGauge.MaximumValue = HoursGauge.MaximumValue = 60;

                // Since we don't want to see these tick marks, lets set the minor tick marks count to zero so no tick marks try to render.
                HoursGauge.MinorTickCount = 0;

                // Setup the Tick marks for the background
                _backgroundGauge.Interval = 1;
                _backgroundGauge.MinorTickCount = 4;

                SetupStyling();

                HoursGauge.Value = 60;
            }
        }

        public void SetupStyling()
        {
            _glassImage = new UIImageView(new RectangleF(0, 0, Frame.Size.Width, Frame.Size.Height))
            {
                AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight,
                ContentMode = UIViewContentMode.ScaleAspectFit,
                Image = UIImage.FromBundle(@"Clock2Shadow.png")
            };
            AddSubview(_glassImage);

            bool iPhone = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone;

            // Make the Number labels render inside of the tickmarks as opposed to outside of them
            _backgroundGauge.LabelExtent = .50f;

            // Move the TickMarks further out from the center of the clock
            _backgroundGauge.TickStartExtent = .59f;
            _backgroundGauge.TickEndExtent = .64f;
            _backgroundGauge.MinorTickStartExtent = .59f;
            _backgroundGauge.MinorTickEndExtent = .64f;

            _backgroundGauge.MaximumValue = 60f;
            _backgroundGauge.Interval = 5f;

            // Set the font for our labels
            _backgroundGauge.Font = UIFont.FromName(@"Verdana", (iPhone) ? 20f : 40f);

            // Move the Backing out and set its colors and stroke thickness
            _backgroundGauge.BackingOuterExtent = .93f;
            _backgroundGauge.BackingStrokeThickness = 0f;
            _backgroundGauge.BackingOutline = _backgroundGauge.BackingBrush = new IGBrush(UIColor.Clear);

            // Make the hours hand smaller
            HoursGauge.NeedleEndExtent = .6f;
            HoursGauge.NeedlePointFeatureExtent = .55f;

            // Change the thickness of the minutes and hours hands.
            HoursGauge.NeedleStartWidthRatio = .03f;
            HoursGauge.NeedleEndWidthRatio = .03f;
            HoursGauge.NeedlePointFeatureWidthRatio = .02f;

            HoursGauge.NeedlePivotWidthRatio = .001f;

            HoursGauge.NeedlePivotBrush = new IGBrush(UIColor.Black);
            HoursGauge.NeedlePivotOutline = new IGBrush(UIColor.White);

            HoursGauge.NeedleShape = IGGaugeNeedleShape.IGGaugeNeedleShapeTriangle;

            HoursGauge.NeedlePivotShape = IGGaugePivotShape.IGGaugePivotShapeCircleUnderlayWithHole;

            var range = new IGGaugeRange
            {
                Outline = new IGBrush(UIColor.Black),
                StrokeThickness = 2,
                StartValue = 0,
                EndValue = 60,
                InnerStartExtent = .59f,
                InnerEndExtent = .59f,
                OuterEndExtent = .59f,
                OuterStartExtent = .59f
            };
            _backgroundGauge.AddRange(range);

            Frame = Frame;
        }
    }

And the usage..

    public class MyViewController : UIViewController
    {
        //declare StopWatch UIView
        private StopWatch _stopWatch;

        //Timer to adjust watch hand
        private NSTimer _timer;

        //Tick count to set value for stopwatch
        private float _tickCount;

        public MyViewController()
        {
        }

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

            View.Frame = UIScreen.MainScreen.Bounds;
            View.BackgroundColor = UIColor.White;
            View.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;

            //Init tickcount
            _tickCount = 60;

            //Init StopWatch
            _stopWatch = new StopWatch
            {
                Frame = new RectangleF(0, 0, this.View.Frame.Size.Width, this.View.Frame.Size.Height),
                AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight,
            };

            View.AddSubview(_stopWatch);

            //Let the countdown begin!
            _timer = NSTimer.CreateRepeatingScheduledTimer(TimeSpan.FromSeconds(.1d), () =>
                {
                    if (_tickCount > 0)
                    {
                        _tickCount -= .1f;
                    }
                    else
                    {
                        _timer.Invalidate();
                        _timer.Dispose();
                    }

                    //The value property of IGGauge determines where the gauge value is set
                    _stopWatch.HoursGauge.Value = _tickCount;
                });
        }    }