Enhanced Monotouch.Dialog FloatElement

For those .NET developers that aren’t yet familiar with the Monotouch framework and are looking to write iOS apps using your existing C#/.NET skill set, you need to check it out. it’s a great platform, especially if you don’t want to deal with the hassle of leaning Objective-C.

Monotouch.Dialog is a specific framework of classes designed to ease the pain usually required in writing the rather repetitive bits of code needed to get tables and other UI paradigms working in iOS. It’s a relatively new addition to the Monotouch API, so there are still some rough edges.

The basic UI range slider (FloatElement) provided by the framework seems to be missing some obvious items, such as the ability to get an event or indication that the value has changed.

I created my own “extended” version with a few more features that I needed for a project I was working on.

Features
  • Ability to ’Lock/Unlock’ the range slider using provided UI images.
  • Inline caption that can be tied directly to the value of the slider
  • Callback functionality when the value of the element is changing
Looking ahead – Future Enhancements

The lock/unlock feature was the precursor for an additional feature I plan on building – a FloatElementGroup. The idea here is that multiple sliders are present and are used to split a given ‘whole’ value by various percentages (think audio equalizers). The sliders would auto-adjust as needed to compensate for any changes to a single slider and the user could optionally lock certain sliders on fixed values.

 

Here’s the code. Hopefully this will come in handy to someone else looking to use a FloatElement.

Source Code
public class FloatElementEx : Element
{
    static NSString skey = new NSString("FloatElementEx");
    const float LockImageWidth = 32.0f;
    const float LockImageHeight = 32.0f;

    /// <summary>
    /// Set a string to reserve a certain amount of space for the 
    /// caption used in the FloatElement. Useful when there is no
    /// initial caption to show - allows space to be reserved for 
    /// when it will be set.
    /// </summary>
    public string ReserveCaptionPlaceholderString { get; set; }
    /// <summary>
    /// Returns the locked status
    /// </summary>
    public bool IsLocked { get { return _valueLocked; } }
    public bool ShowCaption { get; set; }
    /// <summary>
    /// Ties the displayed caption to the value of the slider
    /// </summary>
    public bool UseCaptionForValueDisplay { get; set; }
    public bool Continuous { get; set; }
    public int MinValue { get; set; }
    public int MaxValue { get; set; }
    public int Value { get; private set; }
    public UIImage LockImage { get; set; }
    public UIImage UnlockImage { get; set; }

    private UIButton _lockImageView;
    private UISlider _slider;
    private Action<int> _valueChangedCallback;
    private bool _valueLocked;
    private bool _lockable = false;


    public FloatElementEx(int value, Action<int> valueChanged = null, bool continuous = true, bool lockable = false)
        : base(null)
    {
        MinValue = 0;
        MaxValue = 100;
        Value = value;
        Continuous = continuous;
        _lockable = lockable;
        _valueChangedCallback = valueChanged;
    }

    protected override NSString CellKey { get { return skey; } }

    public override UITableViewCell GetCell(UITableView tv)
    {
        var cell = tv.DequeueReusableCell(CellKey);
        if (cell == null) {
            cell = new UITableViewCell(UITableViewCellStyle.Default, CellKey);
            cell.SelectionStyle = UITableViewCellSelectionStyle.None;
        }
        else
            RemoveTag(cell, 1);

        SizeF captionSize = new SizeF(0, 0);
        if (ShowCaption && (Caption != null || ReserveCaptionPlaceholderString != null || UseCaptionForValueDisplay)) {
            if (Caption == null) {
                if (UseCaptionForValueDisplay)
                    captionSize = cell.TextLabel.StringSize(MaxValue.ToString(), 
                        UIFont.FromName(cell.TextLabel.Font.Name, UIFont.LabelFontSize));
                else if (!string.IsNullOrEmpty(ReserveCaptionPlaceholderString))
                    captionSize = cell.TextLabel.StringSize(ReserveCaptionPlaceholderString, 
                        UIFont.FromName(cell.TextLabel.Font.Name, UIFont.LabelFontSize));
            }
            else {
                captionSize = cell.TextLabel.StringSize(Caption, UIFont.FromName(cell.TextLabel.Font.Name, UIFont.LabelFontSize));
            }

            captionSize.Width += 10; // Spacing

            if (Caption != null)
                cell.TextLabel.Text = Caption;
        }

        var lockImageWidth = _lockable ? LockImageWidth : 0;

        if (_slider == null) {
            _slider = new UISlider(new RectangleF(10f + captionSize.Width, 12f, 280f - captionSize.Width - lockImageWidth, 7f)) {
                BackgroundColor = UIColor.Clear,
                MinValue = this.MinValue,
                MaxValue = this.MaxValue,
                Continuous = this.Continuous,
                Value = this.Value,
                Tag = 1
            };
            _slider.ValueChanged += delegate {
                Value = (int)_slider.Value;
                if (UseCaptionForValueDisplay) {
                    Caption = Value.ToString();
                    // force repaint/redraw
                    if (GetContainerTableView() != null) {
                        var root = GetImmediateRootElement();
                        root.Reload(this, UITableViewRowAnimation.None);
                    }
                }
                if (_valueChangedCallback != null)
                    _valueChangedCallback(Value);
            };
        }
        else {
            _slider.Value = Value;
        }

        if (_lockable){
            if (_lockImageView == null)
                _lockImageView = new UIButton(new RectangleF(_slider.Frame.X + _slider.Frame.Width, 2f, lockImageWidth, LockImageHeight));
            
            _lockImageView.SetBackgroundImage((_valueLocked) ? LockImage : UnlockImage, UIControlState.Normal);
            _lockImageView.TouchUpInside += (object sender, EventArgs e) => {
                _valueLocked = !_valueLocked;
                _lockImageView.SetBackgroundImage((_valueLocked) ? LockImage : UnlockImage, UIControlState.Normal);
                if (_valueLocked)
                    _slider.Enabled = (!_valueLocked);
            };
            cell.ContentView.AddSubview(_lockImageView);
        }
        cell.ContentView.AddSubview(_slider);
        return cell;
    }

    public override string Summary()
    {
        return Value.ToString();
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_slider != null)
            {
                _slider.Dispose();
                _slider = null;
            }
        }
    }

    public void SetValue(int f)
    {
        if (!IsLocked)
            _slider.SetValue(f, false);
    }

    public void SetCaption(string caption)
    {
        Caption = caption;
        // force repaint/redraw
        if (GetContainerTableView() != null) {
            var root = GetImmediateRootElement();
            root.Reload(this, UITableViewRowAnimation.None);
        }
    }
}

Usage
// Basic slider with callback [0..50], not lockable, Caption displayed as slider value
var elem = new FloatElemenEx(50, lockable: false, valueChanged: (val) => DoSomething())
{
    ShowCaption = true,
    UseCaptionForValueDisplay = true
}

// Basic slider with callback [0..50], lockable (can click image), Caption displayed as slider value
var elem = new FloatElemenEx(50, lockable: true, valueChanged: (val) => DoSomething())
{
    ShowCaption = true,
    UseCaptionForValueDisplay = true,
    LockImage = UIImage.FromBundle("images/lock.png"),
    UnlockImage = UIImage.FromBundle("images/unlock.png"),
}

// Basic slider with callback [0..50], not lockable, Caption set explicitly, space reserved
var elem = new FloatElemenEx(50, lockable: false, valueChanged: (val) => DoSomething( DoSomeCalc(elem.SetCaption(val); )))
{
    ShowCaption = true,
    UseCaptionForValueDisplay = false,
    ReserveCaptionPlaceholderString = "XXX", // calculates space based on this string            
}

// Sets the value of the slider and kicks off needed repaints internally (ie. if caption is tied to value, etc)
elem.SetValue(12);

// Sets the sliders caption. Need to use SetCaption() and not the Caption proprerty. Formers refreshes the element, latter doesn't.
elem.SetCaption("Hi!");

C# Version of DocRank

Recently I’ve been exploring Machine Learning concepts (clustering, link analysis, etc.) and discovered a great resource – Algorithms of the Intelligent Web.

The book gives a good introduction and treatment of applied Machine Leaning, especially for someone new to the topic. The book is particularly good in my opinion since it talks about machine learning concepts with an emphasis on how they can be applied to various areas – improving search, providing recommendations, fraud detection, etc. The only drawback to this book as a .NET developer is that the code samples in the book and the source code are all in Java.

Not a big deal, since Java and C# and very close as far as syntax and API structure. I performed a quick and dirty translation of the code from Java to C# if anyone is interested. I haven’t yet made a pass through the code to clean it up or make it more efficient – perhaps in the near future I’ll release an updated version of the source. The C# source for the converted Java code can be downloaded here.