Soup-to-Nuts: Phonebook Module (Part 3)

In this part, we’ll add support for pages to our phonebook. That may not sound like much, but it will set us up for the 4th (and final) part where we wrap everything into a SIMPL User Macro that we can easily drop into our programs.

Before we get into the new stuff, let’s fix a bug we encountered in the last part. Whenever we tried to select the last entry in our list, nothing happened. The data is there, we just can’t select it. See this example:

The select bug in action

See when we tried to select entry 3, nothing happens. When we add a new entry to the end of our list, the Contacts_Updated event handler fires to tell us what we entered and where it was saved. But when we try to select the new entry 4, nothing happens. If we go back and select entry 3, now we can see James Madison was hiding there all along. We have an off-by-one error!

Open Phonebook.cs and fix the Selection property’s set method:

public ushort Selection
{
    get
    {
        if (_selection < 0)
            return 0;
        else
            return (ushort)(_selection + 1);
    }
    set
    {
        if (value <= _entries.Count)
        {
            _selection = value - 1;

            if (_selection < 0)
            {
                SelectedEntryName = "";
                SelectedEntryNumber = "";
            }
            else
            {
                SelectedEntryName = _entries[_selection].Name;
                SelectedEntryNumber = _entries[_selection].Number;
            }

            if (OnSelection != null) OnSelection((ushort)(_selection + 1));
        }
    }
}

Remember, the value that comes from SIMPL+ will be 1 up to the size of our phonebook, so we have to check <= instead of just <. There’s one other place that we need to fix as well:

public void Remove(ushort index)
{
    if ((index > 0) &&
        (index <= _entries.Count))
    {
        _entries.RemoveAt(index - 1);

        if (PhonebookUpdated != null)
        {
            PhonebookUpdated(this, new PhonebookUpdateEventArgs {
                Index = index,
                Name = "",
                Number = "" });            
        }
    }
}

The same bug would have prevented us from removing the last entry in our phonebook. Now that we’re fixed, go ahead and Build Solution, copy modules through the various directories, and test your program again to make sure the bug is fixed:

All 5 entries can be selected now

Creating pages

If we wanted to deal with our contacts as just one long scrolling list, we’d be almost done. The UI I want to show you will be broken into pages, so we need to come up with a way to split it up. We’ll give the user the ability to select different sizes, too. Let’s add a new property to our Phonebook class to do this:

public event PhonebookUpdateEventHandler PhonebookUpdated;

private const ushort MAX_PAGE_SIZE = 500;
private ushort _pageSize;

public ushort PageSize
{
    get
    {
        return _pageSize;
    }
    set
    {
        if ((value > 0) &&
            (value <= MAX_PAGE_SIZE))
        {
            _pageSize = value;
        }
    }
}

private int _selection;

We’ll use MAX_PAGE_SIZE internally to keep some sanity here. Getting PageSize simply returns _pageSize. Setting PageSize makes sure we don’t set it to zero or go beyond the maximum. It’s OK to have a PageSize larger than the number of contacts, we’ll fill in the extra slots with blanks. We’ll also need a way to tell SIMPL+ where we are within the phonebook, so just below PageSize add:

}

public ushort TotalPages
{
    get
    {
        ushort pages;

        pages = (ushort)(_entries.Count / PageSize);

        if (_entries.Count % PageSize > 0)
            pages++;

        if (pages == 0) pages = 1;

        return pages;
    }
}

private ushort _currentPage;

public ushort CurrentPage
{
    get
    {
        return _currentPage;
    }
    set
    {
        if ((value > 0) &&
            (value <= TotalPages))
        {
            _currentPage = value;
        }
    }
}

private int _selection;

If we want to changes pages, we set CurrentPage to something else (greater than zero and less than or equal to TotalPages). We also make sure that TotalPages shows enough to include any extra entries that don’t fill an entire page (the % gives us the remainder after dividing by PageSize). On our UI, we’ll show the user where they currently are within the entire phonebook and allow them to navigate forwards or backwards by pages.

Our Selection property still deals in absolutes: any entry in the entire phonebook can be selected whether it’s visible on the current page or not. We already wrote a bit of code around that idea and it would be a shame to rewrite it now. And, I’d like to keep it available in our library in case we decide to provide a different access method in SIMPL+ later on. So let’s add a SelectPageEntry property that only works within the current page:

}

public ushort SelectPageEntry
{
    get
    {
        if (_selection < 0)
            return 0;

        return (ushort)((_selection % PageSize) + 1);
    }
    set
    {
        if ((value > 0) &&
            (value <= PageSize))
        {
            Selection = (ushort)((CurrentPage - 1) * PageSize + value);
        }
    }
}

public string SelectedEntryName { get; private set; }

We check to make sure SelectPageEntry falls within our PageSize before setting it. To do that, we translate our page offset into an absolute index then add our value to it. We’ll leave a get here for now, but I’m thinking it will go away. It also assumes _selection is on the current page. Let’s make sure to clear our selection, any time we change pages:

public ushort CurrentPage
{
    get
    {
        return _currentPage;
    }
    set
    {
        if ((value > 0) &&
            (value <= TotalPages))
        {
            _currentPage = value;
            Selection = 0;
        }
    }
}

We have the ability to navigate around to different pages now, but we still don’t know what exists on those pages. Let’s trigger a new event when the page updates so that SIMPL+ can be told what exists there. There are many ways to handle this, but I want to try passing as much info into the event handler as possible. I came up with a PhonebookPageEventArgs class that tells us everything about a page in the phonebook. Add this above the Phonebook class:

}

public class PhonebookPageEventArgs : EventArgs
{
    public ushort Page { get; set; }
    public string[] Names { get; set; }
}

public class Phonebook
{

And add the necessary delegate and event to our Phonebook class:

public class Phonebook
{
	public delegate void StatusFeedback (ushort success);
	public delegate void SelectionFeedback (ushort value);
	public delegate void PhonebookUpdateEventHandler (object sender, PhonebookUpdateEventArgs args);
	public delegate void PhonebookPageEventHandler (object sender, PhonebookPageEventArgs args);

	private List<PhonebookEntry> _entries;
	private string _filename;

	public StatusFeedback OnInitialize { get; set; }
	public StatusFeedback OnSave { get; set; }
	public SelectionFeedback OnSelection { get; set; }

	public event PhonebookUpdateEventHandler PhonebookUpdated;
	public event PhonebookPageEventHandler PageUpdated;

	private const ushort MAX_PAGE_SIZE = 500;

I’m going to be lazy and only notify SIMPL+ about page contents if we’re asked for a different page. Setting CurrentPage will become an expensive operation after we add this:

public ushort CurrentPage
{
    get
    {
        return _currentPage;
    }
    set
    {
        if ((value > 0) &&
            (value <= TotalPages))
        {
            _currentPage = value;
            Selection = 0;

            if (PageUpdated != null)
            {
                var args = new PhonebookPageEventArgs();
                args.Page = _currentPage;
                args.Names = new string[_pageSize];

                for (int i = 0; i < _pageSize; i++)
                {
                    int j = (_currentPage - 1) * _pageSize + i;

                    if (j < _entries.Count)
                        args.Names[i] = _entries[j].Name;
                    else
                        args.Names[i] = "";
                }

                PageUpdated(this, args);
            }
        }
    }
}

We package up only the visible names into our args variable then pass it to the PageUpdated event handler. Everything else from here is handled in SIMPL Windows and SIMPL+. Build Solution, then copy SoupToNuts.clz over to your SIMPL Modules directory.

Phonebook Wrapper

We’re going to add page selection to our SIMPL+ module. Add this code to the Phonebook Wrapper v1.0.usp module. For now, we’ll define a fixed page size of 10 entries to keep things simple:

#ENABLE_TRACE

#DEFINE_CONSTANT MAX_PAGE_SIZE 10

#USER_SIMPLSHARP_LIBRARY "SoupToNuts"

Then add an ANALOG_INPUT to select a new page:

// --- Inputs ---

DIGITAL_INPUT Save;

STRING_INPUT  New_Contact_Name[100];
STRING_INPUT  New_Contact_Number[100];
DIGITAL_INPUT Add_New_Contact;

ANALOG_INPUT  Select;
DIGITAL_INPUT Remove_Selected;

ANALOG_INPUT  Current_Page;

// --- Outputs ---

And a couple OUTPUTs to get feedback to our program:

// --- Outputs ---

DIGITAL_OUTPUT Initialized_Fb;
DIGITAL_OUTPUT Save_Fb;

ANALOG_OUTPUT Select_Fb;
STRING_OUTPUT Selected_Entry_Name;
STRING_OUTPUT Selected_Entry_Number;

ANALOG_OUTPUT Current_Page_Fb;
ANALOG_OUTPUT Total_Pages_Fb;

STRING_OUTPUT Page_Entry_Name[MAX_PAGE_SIZE];

// --- Module Parameters ---

Let’s add an event for changing Current_Page:

THREADSAFE PUSH Remove_Selected
{
	If (Contacts.Selection > 0)
		Contacts.Remove(Contacts.Selection);
}

THREADSAFE CHANGE Current_Page
{
	Contacts.CurrentPage = Current_Page;
}

// --- Callbacks ---

This is simple enough, we just pass the value into our Contacts variable and it handles validation for us. Next we should update our library callbacks:

// --- Callbacks ---

CALLBACK FUNCTION Contacts_OnInitialize (INTEGER success)
{
	Contacts.PageSize = MAX_PAGE_SIZE;
	Contacts.CurrentPage = 1;

	Total_Pages_Fb = Contacts.TotalPages;
	Initialized_Fb = success;
}

CALLBACK FUNCTION Contacts_OnSave (INTEGER success)
{
	Save_Fb = success;
}

CALLBACK FUNCTION Contacts_OnSelection (INTEGER value)
{
	Select_Fb = value;
	Selected_Entry_Name = Contacts.SelectedEntryName;
	Selected_Entry_Number = Contacts.SelectedEntryNumber;
}

EVENTHANDLER Contacts_Updated (Phonebook sender, PhonebookUpdateEventArgs args)
{
	Trace("Contact %d updated: %s [ %s ]", args.Index, args.Name, args.Number);

	Total_Pages_Fb = Contacts.TotalPages;
}

EVENTHANDLER Contacts_Page_Updated (Phonebook sender, PhonebookPageEventArgs args)
{
	INTEGER i;

	Current_Page_Fb = args.Page;

	For (i = 1 To sender.PageSize)
	{
		Page_Entry_Name[i] = args.Names[i - 1];
	}
}

// --- Main ---

Here we ensure we start on page 1 after Contacts is initialized. Also, when our contacts are updated, the total number of pages may have updated, so we account for that. Finally, when our page updates, we update our feedback and loop through the names passed in. This way, we should always show which page we’re on and which entries appear there.

Then in Main, we need to make sure we register our new event handler:

// --- Main ---

Function Main()
{
	RegisterDelegate(Contacts, OnInitialize, Contacts_OnInitialize);
	RegisterDelegate(Contacts, OnSave, Contacts_OnSave);
	RegisterDelegate(Contacts, OnSelection, Contacts_OnSelection);

	RegisterEvent(Contacts, PhonebookUpdated, Contacts_Updated);
	RegisterEvent(Contacts, PageUpdated, Contacts_Page_Updated);

	WaitForInitializationComplete();
	Contacts.Initialize(File_Name);
}

That’s it! Save and compile until SIMPL+ picks up the new library and doesn’t complain about errors. Now copy all the module files into your Demo Programs folder and we’ll work on the SIMPL Windows side of things.

Phonebook Demo

Update the signals on your new Phonebook Wrapper module:

Save and Compile this program and load to your processor. Now if we add enough names to our phonebook, we can page around by changing the value of Current_Page:

Pat yourself on the back! We could call this done, but we’ll add some nice features in the next installment like alphabetizing our contacts so we can find them easier. Thanks for reading!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s