comment 1

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

In this part, we’ll continue to develop our Phonebook class to provide more functionality to our SIMPL Windows programs. We won’t quite finish in this part, but we’ll be very close.

Make sure you download the current source code from GitHub, too.

Right now, the Phonebook class tries to load from a file of our choosing when we call Initialize. It reads through the entire file, one line at a time. As it reads each line, it checks for a field delimiter (the | character) and then sticks the first field into Name and the second field into Number. As it’s doing this, it builds a list of PhonebookEntrys we can work with.

The next logical thing to add is a Save method, so lets add that to our Phonebook class. In Phonebook.cs, add:

public void Save()
{
    try
    {
        using (var stream = File.CreateText(_filename))
        {
            foreach (var entry in _entries)
            {
                stream.WriteLine("{0}|{1}", entry.Name, entry.Number);
            }
        }

        if (OnSave != null) OnSave(1);
    }
    catch (Exception e)
    {
        ErrorLog.Error("Exception in Phonebook.Save: {0}",
            e.Message);

        if (OnSave != null) OnSave(0);
    }
}

This method works similarly to Initialize: we open our file for writing (CreateText), loop over every PhonebookEntry in our list, and write each one to a separate line, separating fields with a |. We’ll also call a delegate named OnSave to signal if we successfully wrote out our contacts. Add this delegate to the top of our class:

private string _filename;

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

public Phonebook()
{

At this point, I would normally go through the whole copy-n-paste saga of updating our SIMPL+ module to register the OnSave delegate, expose this feedback to SIMPL Windows, and update the program to see if reading and writing are working in Debugger. But that’s a lot of work that we can save until the end. For now, just rebuild your solution to make sure there are no errors.

Add

We can load and save our contacts to disk, but how do we add new entries to our list? Let’s create an Add method in the Phonebook class:

public void Add(string name, string number)
{
    _entries.Add(new PhonebookEntry { Name = name, Number = number });
}

We create a new PhonebookEntry object and add it to the end of the _entries list. We could stop here and delve into topics on should we validate the name and number before adding it? What if we already have a contact listed under that name? What if our _entries list grows too large? How do we handle these things? Let’s hold off on doing too much with this for now, we can always come back to it later.

Remove

Now that we can add new entries, we should also be able to remove old entries. Create a Remove method in the Phonebook class:

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

We’ll call out the exact index of which entry we want to remove from our list. Why the ushort type? Again, when we call this from SIMPL+, we need an allowed data type we can pass in. We also adjust the index since SIMPL+ arrays start at 1 but SIMPL# arrays start at 0. (There’s a subtle bug here that we’ll fix later.) This leads us to our next requirement: how do we select a contact in our list?

Selection

So far we’ve added methods and delegates to our Phonebook class. Now we need to add a property so we can get and set the selected contact. Add the following accessor to the top of our class:

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

private int _selection;

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

public Phonebook()
{

A few things to note here:

  • We’re storing the actual selection in an int variable named _selection. When we return the value to SIMPL+, we have to cast to a ushort. When we accept new values in, we have to check that we haven’t exceeded the length of our list.
  • See the -1 when we set Selection and +1 when we get Selection? Arrays in SIMPL+ start at 1, but arrays in SIMPL# (sensibly) start at 0. We’re adapting between the two worlds. Also, we’ll consider -1 to mean “no selection” in SIMPL# since this adjusts to 0 in SIMPL+.

We’ll need to know which name and number were selected, so add a few more properties to our class:

}

public string SelectedEntryName { get; private set; }
public string SelectedEntryNumber { get; private set; }

public Phonebook()
{

We can set these when Selection is updated:

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;
            }
        }
    }
}

Remember, we can set Selection = 0 to clear our selection which should clear SelectedEntryName and SelectedEntryNumber as well. Otherwise, we can look up the values from our _entries list. Let’s not forget: we need to signal to SIMPL+ that our selection updated. Let’s create a new delegate type for that:

public class Phonebook
{
    public delegate void StatusFeedback (ushort success);
    public delegate void SelectionFeedback (ushort value);

    private List<PhonebookEntry> _entries;
    private string _filename;

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

    private int _selection;

Then update our Selection property to call the delegate:

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));
        }
    }
}

When the value of Selection changes, we’ll notify SIMPL+ by calling the OnSelection delegate with the adjusted value.

UpdateFeedback

Now we have Add, Remove, and Selection to interact with our list in SIMPL+. How do we let SIMPL+ know that our list contents have updated and should be re-displayed? Another delegate–or rather an event–sounds like the right approach.

We’ll need to provide details about the entry that was updated. For that, lets create a new class named PhonebookUpdateEventArgs just above our Phonebook class:

namespace SoupToNuts.Phonebook
{
    public class PhonebookUpdateEventArgs : EventArgs
    {
        public ushort Index { get; set; }
        public string Name { get; set; }
        public string Number { get; set; }
    }

    public class Phonebook
    {

This class inherits from EventArgs, so that we can properly handle the event callback. In our Phonebook class, we’ll create a new delegate for PhonebookUpdateEventHandler:

public class Phonebook
{
    public delegate void StatusFeedback (ushort success);
    public delegate void SelectionFeedback (ushort value);
    public delegate void PhonebookUpdateEventHandler (object sender, PhonebookUpdateEventArgs 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;

    private int _selection;

Then add calls to the event handler in Add and Remove:

public void Add(string name, string number)
{
    _entries.Add(new PhonebookEntry { Name = name, Number = number });

    if (PhonebookUpdated != null)
    {
        PhonebookUpdated(this, new PhonebookUpdateEventArgs {
            Index = (ushort)_entries.Count,
            Name = name,
            Number = number });
    }
}

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 = "" });            
        }
    }
}

This way we can tell SIMPL+ where the entry was added or removed from our list and what the name and number are (or empty strings if we removed it). Build the solution and copy your output files into the SIMPL Modules directory. Now we need to build up our SIMPL+ module to match.

Phonebook Wrapper

Our SIMPL+ module needs to expose anything our library does in a way that SIMPL Windows can use. We’ve got quite a bit of work to do. Let’s start at the top of Phonebook Wrapper v1.0.usp and work our way down:

#USER_SIMPLSHARP_LIBRARY "SoupToNuts"

// --- 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;

// --- Outputs ---

DIGITAL_OUTPUT Initialized_Fb;
DIGITAL_OUTPUT Save_Fb;

ANALOG_OUTPUT Select_Fb;
STRING_OUTPUT Selected_Entry_Name;
STRING_OUTPUT Selected_Entry_Number;

// --- Module Parameters ---

Here we’ve defined the interface we’ll expose to SIMPL Windows, the module inputs and outputs. Let’s write some event handlers to match:

// --- Global Variables ---

Phonebook Contacts;

// --- Events ---

THREADSAFE PUSH Save
{
	Contacts.Save();
}

THREADSAFE PUSH Add_New_Contact
{
	Contacts.Add(New_Contact_Name, New_Contact_Number);
}

THREADSAFE CHANGE Select
{
	Contacts.Selection = Select;
}

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

// --- Callbacks ---

I’m putting THREADSAFE on each of these event handlers because I want to make sure the calls into our library finish before another event is processed. We have a number of delegates we need to handle as well:

// --- Callbacks ---

CALLBACK FUNCTION Contacts_OnInitialize (INTEGER success)
{
	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);
}

// --- Main ---

And lastly, we need to update the Main function to register these delegates:

// --- Main ---

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

	RegisterEvent(Contacts, PhonebookUpdated, Contacts_Updated);

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

Save and Compile your module to make sure no errors occur… sometimes you have to do it twice to get the new SoupToNuts.clz to link correctly. I’m not sure why. Take your compiled module files and copy them into the Demo Programs folder. Now to update the Phonebook Demo v1.0.smw program!

Phonebook Demo

You might have to delete the old module symbol from your program and re-add it. Give it the following signal names, Save and Compile, then load to your Crestron processor of choice:

You might notice the inputs and outputs of our module decide on their own the order in which they end up. It’s OK. We’ll eventually wrap this in a SIMPL Macro to clean it up once we’re satisfied with the way it works.

SIMPL Debugger

Fire up SIMPL Debugger in Toolbox, and you will notice a few more signals available now:

You can add a few contacts manually if you like. Select New_Contact_Name$ and give it a name (George Washington), then New_Contact_Number$ (555-1111), then push Add_New_Contact. In the trace window you should see:

The Trace message was typed into our event handler. In the future, we’ll use that to update our UI but for now a message in Debugger lets us know we at least got that far.

Add a couple more contacts this way. I suggest John Adams, Thomas Jefferson, and James Madison. Then press the Save signal. Save_Fb should go high. Now if you set Select to 3, Select_Fb should go to 3, Selected_Entry_Name$ and Selected_Entry_Number$ should match the 3rd contact in your list:

If you restart the program now, you should still be able to retrieve your contacts (since they load from disk when the program starts):

You might notice we have a bug when we try to select the last entry in our list. We can fix that in the next part. Try to delete entries and see if that works (i.e.: set Select to 1, push Remove_Selected, then set Select to 1 again to see what changes).

In the next part, we’ll add pages to our module so we can work on developing a UI to navigate through our phonebook contacts. Thanks for reading!

1 Comment so far

  1. Michel

    Hi,
    Learned a lot from your soup-to-nuts. Many thanks for that!
    I’m a experienced Crestron programmer who’s trieing to get his head arround C#

    Kind Regards, Michel

    Liked by 1 person

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 )

Google photo

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

Twitter picture

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

Facebook photo

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

Connecting to %s