This example finishes the series showing how to make a C# program send an SMS (Short Message Service) message. You could use this technique to make a program monitor some sort of ongoing process and send you a message if there is a problem.
The example How to extract only some of the information from a JSON file in C# demonstrates how you can download a JSON file that contains information about SMS gateway email addresses and extract the carrier and email information. The example How to send an email message in C# shows how to send an email.
This example combines the techniques demonstrated in those examples to send an SMS message.
When the program starts, it uses the techniques demonstrates by the first example to get the SMS carrier information.
The program executes the following code when you fill in the information and click Send.
// Send the message.
private void btnSend_Click(object sender, EventArgs e)
{
try
{
string carrier_email = cboEmail.SelectedItem.ToString();
string phone = txtPhone.Text.Trim().Replace("-", "");
phone = phone.Replace("(", "").Replace(")", "").Replace("+", "");
string to_email = phone + "@" + carrier_email;
SendEmail(txtToName.Text, to_email,
txtFromName.Text, txtFromEmail.Text,
txtHost.Text, int.Parse(txtPort.Text),
chkEnableSSL.Checked, txtPassword.Text,
txtSubject.Text, txtBody.Text);
MessageBox.Show("Message sent");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
The program first gets the SMS carrier's email address from the cboEmail ComboBox. The email address the program uses must not contain any characters other than digits so the code trims the address to remove spaces. It also removes the -, (, ), and + characters.
Next the code appends the @ symbol and the carrier's SMS gateway email address. The result should look something like this:
2345678901@sms.airfiremobile.comThe program finishes by calling the SendEmail method described in the second post mentioned above to send the message to this email address. That's all there is to it! The tools you need to download the JSON carrier data, parse the data, and send the email message are somewhat involved. Once you've built those tools, however, sending the SMS message is comparatively easy.
The next tool you need to make a program send an SMS message is a method for sending an email. In recent years this has become harder because there are few email providers willing to let you send anonymous emails. Now to send an email, you need to create a NetworkCredential object that specifies your email server, user name, and password.
This example uses the following SendEmail method to send an email.
using System.Net;
using System.Net.Mail;
// Send an email message.
private void SendEmail(string to_name, string to_email,
string from_name, string from_email,
string host, int port, bool enable_ssl, string password,
string subject, string body)
{
// Make the mail message.
MailAddress from_address = new MailAddress(from_email, from_name);
MailAddress to_address = new MailAddress(to_email, to_name);
MailMessage message = new MailMessage(from_address, to_address);
message.Subject = subject;
message.Body = body;
// Get the SMTP client.
SmtpClient client = new SmtpClient()
{
Host = host,
Port = port,
EnableSsl = enable_ssl,
UseDefaultCredentials = false,
Credentials = new NetworkCredential(from_address.Address, password),
};
// Send the message.
client.Send(message);
}
The method creates MailAddress objects to represent the From and To email addresses. You can use strings for these when you make the MailMessage but then you can't associate a human-friendly name (like "Rod Stephens") with the email addresses.
Next the program creates a MailMessage, passing its constructor the From and To addresses. The code then sets the MailMessage's Subject and Body fields.
The method then creates an SmtpClient object. It sets the client's host and port so it knows where to send the email. The method sets the EnableSsl property according to the value it was passed as a parameter. If your email server uses SSL (Secure Sockets Layer--Gmail uses this), check the box on the form so this is set to true.
The code also sets UseDefaultCredentials to false and sets the client's Credentials property to a new NetworkCredential object containing your email user name and password.
Finally the method calls the SmtpClient's Send method to send the email.
The System.Net.Mail.MailMessage class supports some other features that you might want to use. For example, it has CC and Bcc properties that let you send courtesy copies and blind courtesy copies. The version shown here is good enough for the next post, which sends an SMS message.
The following code shows how the program calls the SendEmail method.
// Send the message.
private void btnSend_Click(object sender, EventArgs e)
{
try
{
SendEmail(txtToName.Text, txtToEmail.Text,
txtFromName.Text, txtFromEmail.Text,
txtHost.Text, int.Parse(txtPort.Text),
chkEnableSSL.Checked, txtPassword.Text,
txtSubject.Text, txtBody.Text);
MessageBox.Show("Message sent");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
This code simply calls SendEmail passing it the values you entered on the form.
Note that the form's initial values for host and port (smtp.gmail.com and 587) work for Gmail. If you want to use some other email host, you'll need to change those values.
The next post will bring together the results of this post and the previous JSON posts to send an SMS message.
Recall from my post Anatomy of an example that my original goal with this series of articles was to write a program to send an SMS message. To do that, I want to download a file listing SMS gateway email addresses. The example Download and display a text file whenever a program starts in C# shows how to download the file.
That file is a JSON format file. The example Use JSON to serialize and deserialize objects in C# shows how to use JSON to serialize and deserialize objects. You could use that technique to deserialize the SMS gateway file. You would define an object that represents the kinds of data contained in the SMS gateway file and then simply deserialize the file.
After staring for quite a while at the file, I decided that this would be fairly hard. It's a big file with a complex structure and it contains lots of information that I don't really need, so I decided on a different approach. (One that also makes a good blog post.)
Instead of trying to understand the structure of the entire JSON file, this example reads it without really knowing what it represents and then sifts through it to find the data it needs.
The following text shows the structure of the JSON file (with LOTS of data omitted).
{
"info" : "JSON array ...",
"license" : "MIT or LGPL...",
"lastupdated" : "2012-07-01",
"countries" : {
"us" : "United States",
"ca" : "Canada",
"ar" : "Argentina",
"aw" : "Aruba",
...
},
"sms_carriers" : {
"us" : {
"airfire_mobile" : ["Airfire Mobile",
"{number}@sms.airfiremobile.com"],
"alltel" : ["Alltel", "{number}@message.alltel.com"],
...
"at_and_t_mobility" : ["AT&T Mobility (Cingular)",
"{number}@txt.att.net",
"{number}@cingularme.com",
"{number}@mobile.mycingular.com"],
...
},
"ca" : {
"aliant" : ["Aliant", "{number}@chat.wirefree.ca"],
...
},
...
},
"mms_carriers" : {
"us" : {
"alltel" : ["Alltel", "{number}@mms.alltel.com"],
...
},
...
}
}
Notice that the us/at_and_t_mobility carrier supports three email addresses. Normally you can use the first one if a carrier has more than one email address, but the program displays them all in case you know which one you want to use.
The following code shows the two classes that the program uses to store the information it gets from the file.
public class CarrierInfo
{
public string CarrierAbbreviation, CarrierName;
public List<string> Emails = new List<string>();
public override string ToString()
{
return CarrierName;
}
}
public class CountryInfo
{
public string CountryAbbreviation, CountryName;
public List<CarrierInfo> Carriers = new List<CarrierInfo>();
public override string ToString()
{
return CountryName;
}
}
The CarrierInfo class stores a cell phone carrier's abbreviation and name, and a list of supported SMS gateway email addresses.
The CountryInfo class stores a country's abbreviation and name, and a list of carriers that are available in that country.
The code that reads the data is fairly long but not super complicated. Basically it loads the data into a dictionary where the keys are strings and the values are objects. Many of the values are also dictionaries with a similar structure.
For example, the file's top-level data is stored in a dictionary with keys info, license, lastupdated, countries, sms_carriers, and mms_carriers. The sms_carriers entry is a dictionary with keys us, ca, and other country abbreviations. Each of the entries in the sms_carriers dictionary is another dictionary with keys that are carrier abbreviations and with values that are arrays holding a carrier's name and email addresses.
The following code shows how the program reads the data.
// Add a reference to System.Web.Extensions.dll.
using System.Web.Script.Serialization;
using System.IO;
using System.Net;
...
private void Form1_Load(object sender, EventArgs e)
{
// Get the data file.
const string url = "https://raw.github.com/cubiclesoft/" +
"email_sms_mms_gateways/master/sms_mms_gateways.txt";
string serialization = GetTextFile(url);
// Add a reference to System.Web.Extensions.dll.
JavaScriptSerializer serializer = new JavaScriptSerializer();
Dictionary<string, object> dict =
(Dictionary<string, object>)serializer.DeserializeObject(serialization);
// Get the countries.
Dictionary<string, CountryInfo> country_infos =
new Dictionary<string, CountryInfo>();
Dictionary<string, object> countries =
(Dictionary<string, object>)dict["countries"];
foreach (KeyValuePair<string, object> pair in countries)
{
CountryInfo country_info = new CountryInfo()
{ CountryAbbreviation = pair.Key, CountryName = (string)pair.Value };
country_infos.Add(country_info.CountryAbbreviation, country_info);
}
// Get the SMS carriers.
Dictionary<string, object> sms_carriers =
(Dictionary<string, object>)dict["sms_carriers"];
foreach (KeyValuePair<string, object> pair in sms_carriers)
{
// Get the corresponding CountryInfo.
CountryInfo country_info = country_infos[pair.Key];
// Get the country's carriers.
Dictionary<string, object> carriers =
(Dictionary<string, object>)pair.Value;
foreach (KeyValuePair<string, object> carrier_pair in carriers)
{
// Create a CarrierInfo for this carrier.
CarrierInfo carrier_info = new CarrierInfo()
{ CarrierAbbreviation = carrier_pair.Key };
country_info.Carriers.Add(carrier_info);
object[] carrier_values = (object[])carrier_pair.Value;
carrier_info.CarrierName = (string)carrier_values[0];
for (int email_index = 1; email_index <
carrier_values.Length; email_index++)
{
string email = (string)carrier_values[email_index];
carrier_info.Emails.Add(email.Replace("{number}", ""));
}
}
}
// Display the countries.
cboCountry.Items.Clear();
foreach (CountryInfo country in country_infos.Values)
{
cboCountry.Items.Add(country);
}
// Make an initial selection.
cboCountry.SelectedIndex = 0;
}
The program defines the URL where it will get the file. It then uses the GetTextFile method described in Download and display a text file whenever a program starts in C# to get the file.
Next the code creates a JavaScriptSerializer. Unlike the serializers described in the previous example that serializes and deserializes objects, this serializer doesn't know what kind of object it is deserializing.
The program call's the serializer's DeserializeObject method. That method returns a Dictionary that contains strings associated with objects. The objects hold various kinds of data depending on what's in the JSON file.
This example gets the "countries" entry from the dictionary. This entry is another dictionary that contains the abbreviations and names of countries used by the JSON file.
The program loops through the country dictionary's key/value pairs. For each country pair, the program stores the country's abbreviation and name in a CountryInfo object. It stores the new CountryInfo object in a dictionary named country_infos using the country's abbreviation as the key.
Next the program returns to the dictionary that represents the JSON file's highest level of data and gets the sms_carriers entry in that dictionary. This entry is another dictionary that holds information about the carriers that are represented in the file.
The program loops over the carrier information. Because the carriers are grouped by country, the keys in the key/value pairs are country abbreviations. The program uses those to look up the corresponding CountryInfo object in the country_infos dictionary.
Next the program uses the value part of the carrier's key/value pair to get the information about the carrier. It creates a CarrierInfo object and adds it to the appropriate CountryInfo object's Carriers list. Finally the code adds the email addresses for the carrier to the CarrierInfo object's Emails list.
The method then displays the names of the countries it loaded in the cboCountry ComboBox. It finishes by selecting the first country in the ComboBox so the program always has country selected.
The program's remaining code updates its ComboBox's when the user makes a selection. When the user selects a country, the carriers ComboBox displays a list of the carriers available in that country. When the user selects a carrier, the email addresses ComboBox displays a list of email addresses provided by that carrier.
The following code shows how the program responds when the user picks a country.
// Display the selected country's carriers.
private void cboCountry_SelectedIndexChanged(object sender, EventArgs e)
{
if (cboCountry.SelectedIndex < 0)
{
cboCarrier.SelectedIndex = -1;
}
else
{
// Get the selected CountryInfo object.
CountryInfo country = cboCountry.SelectedItem as CountryInfo;
Console.WriteLine("Country: " + country.CountryAbbreviation +
": " + country.CountryName);
// Display the CountryCarrier's carriers.
cboCarrier.DataSource = country.Carriers;
}
}
If the user has selected a country, the program converts the selected country into its corresponding CountryInfo object. It then sets the cboCarrier ComboBox's DataSource property to the country's Carriers property. That property is a list of CarrierInfo objects so the ComboBox displays them.
When the user picks a carrier from the carriers ComboBox, the following code executes.
// Display the selected carrier's emails addresses.
private void cboCarrier_SelectedIndexChanged(object sender, EventArgs e)
{
if (cboCarrier.SelectedIndex < 0)
{
cboEmail.SelectedIndex = -1;
}
else
{
// Get the selected CarrierInfo object.
CarrierInfo carrier = cboCarrier.SelectedItem as CarrierInfo;
Console.WriteLine("Carrier: " + carrier.CarrierName);
// Display the Carrier's email addresses.
cboEmail.DataSource = carrier.Emails;
}
}
This code converts the selected carrier into its CarrierInfo object. It then sets the email address ComboBox's DataSource property to the CarrierInfo object's Emails property so it displays the list of available email addresses.
The program doesn't do anything when the user selects an email address.
At this point, you can download the SMS gateway file and get the information you need out of it to find an SMS gateway email address. The next step is to send email to that address. The next two posts explain how to do that.
JSON (JavaScript Object Notation) is a standard for textual storage and interchange of information, much as XML is. Before you roll your eyes and ask if we really need another language to do what XML does, consider how verbose XML is. Even relatively simple object hierarchies can take up a considerable amount of space when represented by XML. JSON is a more compact format that stores the same kinds of information in less space.
When XML first came out, I thought to myself, "This is a really verbose language. It could be so much more concise. I guess people are willing to spend the extra space to get a more readable format. And after all, storage space is cheaper and network speed is faster than ever before." If you remember having similar thoughts, *now* you can roll your eyes.
JSON's basic data types are:
{
"City":"Bugsville",
"EmailAddresses":
[
"somewhere@someplace.com",
"nowhere@noplace.com",
"root@everywhere.org"
],
"Name":"Rod Stephens",
"Orders":
[
{"Description":"Pencils, dozen","Price":1.13,"Quantity":10},
{"Description":"Notepad","Price":0.99,"Quantity":3},
{"Description":"Cookies","Price":3.75,"Quantity":1}
],
"PhoneNumbers":
[
{"Key":"Home","Value":"111-222-3333"},
{"Key":"Cell","Value":"222-333-4444"},
{"Key":"Work","Value":"333-444-5555"}
],
"State":"CA",
"Street":"1337 Leet St",
"Zip":"98765"
}
Some of the fields such as City, Name, and State are simple string values. The EmailAddresses value is an array of strings.
The Orders value is an array of Order objects, each of which has a Description, Price, and Quantity. (Really this should be a collection of Order objects, each of which has a collection of OrderItem objects that have Description, Price, and Quantity properties, but the structure shown here is complicated enough already.)
The PhoneNumbers value is a dictionary where the keys are phone number types (Home, Cell, Work) and the values are the phone number strings.
This example builds a Customer object. It then serializes it into a JSON format and then deserializes it to re-create the original object.
The following code shows the Order class.
// Add a reference to System.Runtime.Serialization.
using System.Runtime.Serialization;
namespace howto_use_json
{
[Serializable]
public class Order
{
[DataMember]
public string Description;
[DataMember]
public int Quantity;
[DataMember]
public decimal Price;
// Return a textual representation of the order.
public override string ToString()
{
decimal total = Quantity * Price;
return Description + ": " +
Quantity.ToString() + " @ " +
Price.ToString("C") + " = " +
total.ToString("C");
}
}
}
To allow the program to serialize Order objects, the class must be marked with the Serializable attribute. That attributes is defined in the System.Runtime.Serialization namespace so the code includes a using statement to make using that namespace easier. To use the namespace, you also need to add a reference to System.Runtime.Serialization at design time.
The fields within the class are marked with the DataMember attribute so the serializer knows to serialize them.
The last part of the class is a ToString method that simply returns a textual representation of the object's values.
The following code shows the Customer class.
// Add a reference to System.Runtime.Serialization.
using System.Runtime.Serialization;
// Add a reference to System.ServiceModel.Web.
using System.Runtime.Serialization.Json;
using System.IO;
namespace howto_use_json
{
[Serializable]
public class Customer
{
[DataMember]
public string Name = "";
[DataMember]
public string Street = "";
[DataMember]
public string City = "";
[DataMember]
public string State = "";
[DataMember]
public string Zip = "";
[DataMember]
public Dictionary<string, string> PhoneNumbers = new Dictionary<string, string>();
[DataMember]
public List<string> EmailAddresses = new List<string>();
[DataMember]
public Order[] Orders = null;
... (Other code shown later) ...
}
}
This class starts much as the Order class does. It also uses the System.Runtime.Serialization namespace so it includes the corresponding using statement.
The class also uses the System.Runtime.Serialization.Json namespace (which requires you to add a reference to System.ServiceModel.Web) and the System.IO namespaces so it also includes using statements for them.
The class's definition starts by defining some simple properties such as Name, Street, and City.
The class defines the PhoneNumbers member as a Dictionary<string, string>. It defines the EmailAddresses member as List<string>. The serializer automatically converts these into the appropriate JSON types.
To make serializing and deserializing objects easier, the class includes the methods ToJson and FromJson. The ToJson method shown in the following code returns a JSON representation of an object.
// Return the JSON serialization of the object.
public string ToJson()
{
// Make a stream to serialize into.
using (MemoryStream stream = new System.IO.MemoryStream())
{
// Serialize into the stream.
DataContractJsonSerializer serializer
= new DataContractJsonSerializer(typeof(Customer));
serializer.WriteObject(stream, this);
stream.Flush();
// Get the result as text.
stream.Seek(0, SeekOrigin.Begin);
using (StreamReader reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
}
This method creates a MemoryStream in which to write. It then creates a DataContractJsonSerializer object to serialize the Customer object. It calls the serializer's WriteObject method to write the current Customer object into the stream and flushes the stream.
Next the code rewinds the stream to the beginning, creates an associated StreamReader, and uses the reader's ReadToEnd method to get the serialization out of the stream and return it. (This seems needlessly awkward to me. If you find a simpler method, email me.
The static FromJson method shown in the following code takes a JSON serialization of a Customer object, uses it to re-create the object, and returns the new object.
// Create a new Customer from a JSON serialization.
public static Customer FromJson(string json)
{
// Make a stream to read from.
MemoryStream stream = new MemoryStream();
StreamWriter writer = new StreamWriter(stream);
writer.Write(json);
writer.Flush();
stream.Position = 0;
// Deserialize from the stream.
DataContractJsonSerializer serializer
= new DataContractJsonSerializer(typeof(Customer));
Customer cust = (Customer)serializer.ReadObject(stream);
// Return the result.
return cust;
}
This method creates a MemoryStream and an associated StreamWriter. It uses the writer to write the JSON serialization into the stream. It then flushes the writer and rewinds the stream.
Next the code creates a DataContractJsonSerializer object to deserialize the Customer object. It calls the serializer's ReadObject to read the object's serialization from the stream, casts the result into a Customer object, and returns the object.
The ToString method shown in the following code is the last piece of the Customer class. It simply returns a textual representation of the Customer object.
// Return a newline separated text representation.
public override string ToString()
{
// Display the basic information.
string result = "Customer" + Environment.NewLine;
result += " " + Name + Environment.NewLine;
result += " " + Street + Environment.NewLine;
result += " " + City + " " + State + " " + Zip + Environment.NewLine;
// Display phone numbers.
result += " Phone Numbers:" + Environment.NewLine;
foreach (KeyValuePair pair in PhoneNumbers)
{
result += " " + pair.Key + ": " + pair.Value + Environment.NewLine;
}
// Display email addresses.
result += " Email Addresses:" + Environment.NewLine;
foreach (string address in EmailAddresses)
{
result += " " + address + Environment.NewLine;
}
// Display orders.
result += " Orders:" + Environment.NewLine;
foreach (Order order in Orders)
{
result += " " + order.ToString() + Environment.NewLine;
}
return result;
}
The ToString method is a bit long but reasonably straightforward.
The following shows how the main program demonstrates JSON serialization by serializing and deserializing a Customer object.
private void Form1_Load(object sender, EventArgs e)
{
// Make an object to serialize.
Customer cust = new Customer()
{
Name = "Rod Stephens",
Street = "1337 Leet St",
City = "Bugsville",
State = "CA",
Zip = "98765",
PhoneNumbers = new Dictionary()
{
{"Home", "111-222-3333"},
{"Cell", "222-333-4444"},
{"Work", "333-444-5555"},
},
EmailAddresses = new List()
{
"somewhere@someplace.com",
"nowhere@noplace.com",
"root@everywhere.org",
},
};
cust.Orders = new Order[3];
cust.Orders[0] = new Order()
{
Description = "Pencils, dozen",
Quantity = 10,
Price = 1.13m
};
cust.Orders[1] = new Order()
{
Description = "Notepad",
Quantity = 3,
Price = 0.99m
};
cust.Orders[2] = new Order()
{
Description = "Cookies",
Quantity = 1,
Price = 3.75m
};
// Display the serialization.
string serialization = cust.ToJson();
txtJson.Text = serialization;
txtJson.Select(0, 0);
// Deserialize.
Customer new_cust = Customer.FromJson(serialization);
txtProperties.Text = new_cust.ToString();
txtProperties.Select(0, 0);
}
The form's Load event handler starts by initializing a Customer object complete with phone numbers, email addresses, and Order objects.
Next the code calls the Customer object's ToJson method to get its serialization and display it in the txtJson TextBox. (Notice that the JSON serialization in the picture doesn't include nice line breaks and formatting to align the data nicely. It sacrifices that to save space.)
The code then calls the Customer class's FromJson method to deserialize the serialization and create a new Customer object. It finishes by using the new object's ToString method to show its properties in the txtProperties TextBox.
This example may seem complicated but it's actually not too bad. It's a bit longer than it might otherwise be because the Customer and Order classes include several useful data types such as a list, dictionary, and array of objects. The actual JSON code in the ToJson and FromJson methods is somewhat confusing but it's fairly short. Those methods also make using JSON to serialize and deserialize objects easy in practice.
When this example program starts, it downloads a file from the internet and displays it. You could do something similar to display a message of the day or announcements for your program.
The program uses the following Load event handler to start the process.
// Download and display the text file.
private void Form1_Load(object sender, EventArgs e)
{
const string url = "https://raw.github.com/cubiclesoft/email_sms_mms_gateways/master/sms_mms_gateways.txt";
txtFile.Text = GetTextFile(url);
txtFile.Select(0, 0);
}
The code defines the URL of the file to download. It then calls the GetTextFile method described next to download the file and displays the result in the txtFile TextBox.
The following code shows the GetTextFile method.
using System.IO;
using System.Net;
...
// Get the text file at a given URL.
private string GetTextFile(string url)
{
try
{
url = url.Trim();
if (!url.ToLower().StartsWith("http")) url = "http://" + url;
WebClient web_client = new WebClient();
MemoryStream image_stream = new MemoryStream(web_client.DownloadData(url));
StreamReader reader = new StreamReader(image_stream);
string result = reader.ReadToEnd();
reader.Close();
return result;
}
catch (Exception ex)
{
MessageBox.Show("Error downloading file " +
url + '\n' + ex.Message,
"Download Error",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
return "";
}
The code first uses the string class's Trim method to remove leading and trailing whitespace from the URL (just in case). Then if the URL doesn't begin with "http," the code adds "http://" to the beginning of the URL. (The code only checks for "http" so it can also handle "https" as in the URL used by this example.)
Next the code creates a WebClient object. It uses the client's DownloadData method to get the file. DownloadData returns an array of bytes so the program creates a memory stream associated with the array and then makes a StreamReader to read the memory stream. The code calls the reader's ReadToEnd method and closes the reader. Finally it returns the string read by the StreamReader.
There are several ways this method could fail (file doesn't exist, improper permissions, not connected to the network, network failure, and so on) so the code does all its work within a try-catch block.
With a it more work, you can download other files such as pictures or RTF format files to display more than just text. If you really want a fancy announcement page downloaded from the internet, you might be better off placing a WebBrowser control on the form and making it navigate to a web page.
If you examine the file that this example downloads, you'll see that it contains JSON-formatted data listing SMS gateway email addresses. My next blog post provides a brief introduction to JSON. The post after that explains how to read the data in the text file that this example downloads and displays.
MCSD Certification Toolkit (Exam 70-483): Programming in C#The link above is for the paperback edition, but a Kindle edition is also available. To help get some reviews going, I'll announce a drawing for five free copies of this book in the next few days. Stay tuned...
By Tiberiu Covaci, Rod Stephens, Vincent Varallo, and Gerry O'Brien
$59.99, 648 pages, May 2012
ASIN: B00CP2PZB0
Congratulations to the following winners of the drawing for copies of Visual Basic 2012 Programmer's Reference:
// Make the ListBox owner-drawn and give it data.
private void Form1_Load(object sender, EventArgs e)
{
lstBooks.DrawMode = DrawMode.OwnerDrawVariable;
lstBooks.Items.AddRange(Values);
}
This code makes the ListBox owner-drawn and gives it some data. The variable Values is an array of arrays of strings. In other words, it contains values for each row and each row holds an array of strings.
When you use an owner-drawn ListBox, you must handle two key events: MeasureItem and DrawItem. The MeasureItem event handler shown in the following code is called to tell the owner-drawn ListBox how much room it should leave for a menu item.
// Row and column sizes.
private float RowHeight, RowWidth;
private float[] ColWidths = null;
private const float RowMargin = 10;
private const float ColumnMargin = 10;
// Return the desired size of an item.
private void lstBooks_MeasureItem(object sender, MeasureItemEventArgs e)
{
// Measure the data if we haven't already done so.
if (ColWidths == null)
{
// Get the row and column sizes.
GetRowColumnSizes(e.Graphics, lstBooks.Font, Values, out RowHeight, out ColWidths);
// Add margins.
for (int i = 0; i < ColWidths.Length; i++) ColWidths[i] += ColumnMargin;
RowHeight += RowMargin;
// Get the total row width.
RowWidth = ColWidths.Sum();
}
// Set the desired size.
e.ItemHeight = (int)RowHeight;
e.ItemWidth = (int)RowWidth;
}
If ColWidths is null, then the program has not yet measured the data. In that case, this code calls the GetRowColumnSizes method to calculate the maximum row height and the column widths for the data values. For more information on this method, see the previous example Draw rows of data in left and right justified columns in C#.
The code then adds a margin to each column's width and to the maximum row height so the values aren't too crowded when they are drawn.
Finally the event handler sets e.ItemHeight and e.ItemWidth to tell the owner-drawn control how much room to allow for the menu item.
The following DrawItem event handler is called when the owner-drawn ListBox needs to draw a menu item.
// Draw an item.
private void lstBooks_DrawItem(object sender, DrawItemEventArgs e)
{
string[] values = (string[])lstBooks.Items[e.Index];
e.DrawBackground();
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
DrawRow(e.Graphics, lstBooks.Font, SystemBrushes.HighlightText, null,
e.Bounds.X, e.Bounds.Y, RowHeight, ColWidths,
VertAlignments, HorzAlignments, values, false);
}
else
{
DrawRow(e.Graphics, lstBooks.Font, Brushes.Black, null,
e.Bounds.X, e.Bounds.Y, RowHeight, ColWidths,
VertAlignments, HorzAlignments, values, false);
}
}
This code gets the data for the ListBox row that it needs to draw. It then calls e.DrawBackground to draw an appropriate background. (If the item is selected (the user clicked it), the background is blue. If the item is not selected, the background is white.)
The code then calls the DrawRow method to draw the row in the ListBox. If the ListBox item is selected (the user clicked it), then it draws the item with the system-defined text highlight color. If the item is not selected, the code calls DrawRow to draw the row with black text.
The code also passes the call to DrawBox the X and Y coordinates of the row's upper left corner as given by the event handler's e.Bounds parameter.
The following code shows the DrawRow method.
// Draw the items in columns.
private void DrawRow(Graphics gr, Font font, Brush brush, Pen box_pen,
float x0, float y0, float row_height, float[] col_widths,
StringAlignment[] vert_alignments, StringAlignment[] horz_alignments,
string[] values, bool draw_box)
{
// Create a rectangle in which to draw.
RectangleF rect = new RectangleF();
rect.Height = row_height;
using (StringFormat sf = new StringFormat())
{
float x = x0;
for (int col_num = 0; col_num < values.Length; col_num++)
{
// Prepare the StringFormat and drawing rectangle.
sf.Alignment = horz_alignments[col_num];
sf.LineAlignment = vert_alignments[col_num];
rect.X = x;
rect.Y = y0;
rect.Width = col_widths[col_num];
// Draw.
gr.DrawString(values[col_num], font, brush, rect, sf);
// Draw the box if desired.
if (draw_box) gr.DrawRectangle(box_pen,
rect.X, rect.Y, rect.Width, rect.Height);
// Move to the next column.
x += col_widths[col_num];
}
}
}
This method makes a RectangleF and a StringFormat that it will use to draw the row's items and then loops through those items. For each item, the code sets the StringFormat's Alignment and LineAlignment properties and draws the item at the appropriate location. If the draw_box parameter is true, the code also draws the RectangleF. (This is mostly for debugging.)
After it draws each item, it increases the X coordinate so it is ready to draw the next item.
This example is fairly complicated but still manageable. An alternative would be to use a ListView control instead of a ListBox. The behavior isn't quite the same so it doesn't provide exactly the same look and feel, but the ListView lets you set up more of the details about column justification interactively at design time.
// The values.
private string[][] Values =
{
new string[] { "C# 24-Hour Trainer", ... , "10/10/2010" },
new string[] { "Beginning Database Design Solutions", ... , "8/8/2008" },
...
};
// The column alignments.
private StringAlignment[] VertAlignments =
{
StringAlignment.Center,
StringAlignment.Center,
...
};
private StringAlignment[] HorzAlignments =
{
StringAlignment.Near,
StringAlignment.Near,
StringAlignment.Far,
StringAlignment.Far,
StringAlignment.Far,
};
The data that will be drawn is an array of arrays of strings. In other words, each row is represented by an array of string values.
The VertAlignments array holds the vertical alignments that should be used to draw each row. The HorzAlignments array holds the horizontal alignments for the data's columns.
The most interesting part of the program involves two methods: GetRowColumnSizes and DrawTextRowsColumns. The following code shows the GetRowColumnSizes method, which returns the data's maximum row height and the widths of the data's columns.
// Return the items' sizes.
private void GetRowColumnSizes(Graphics gr, Font font,
string[][] values, out float max_height, out float[] col_widths)
{
// Make room for the column sizes.
int num_cols = values[0].Length;
col_widths = new float[num_cols];
// Examine each row.
max_height = 0;
foreach (string[] row in values)
{
// Measure the row's columns.
for (int col_num = 0; col_num < num_cols; col_num++)
{
SizeF col_size = gr.MeasureString(row[col_num], font);
if (col_widths[col_num] < col_size.Width)
col_widths[col_num] = col_size.Width;
if (max_height < col_size.Height)
max_height = col_size.Height;
}
}
}
This method takes as parameters a Graphics object similar to the one that the program will later use to draw the data and the font that it will use. The code loops through the rows of data. For each row, it loops through the row's columns. For each column entry, the code uses the Graphics object's MeasureString method to see how big that entry will be when displayed and it updates the maximum row height and that column's width.
The max_height and col_width parameters are output parameters so the calling code learns the maximum row height and the column widths.
The following code shows the DrawTextRowsColumns method.
// Draw the items in columns.
private void DrawTextRowsColumns(Graphics gr, Font font, Brush brush, Pen box_pen,
float x0, float y0, float row_height, float[] col_widths,
StringAlignment[] vert_alignments, StringAlignment[] horz_alignments,
string[][] values, bool draw_box)
{
// Create a rectangle in which to draw.
RectangleF rect = new RectangleF();
rect.Height = row_height;
using (StringFormat sf = new StringFormat())
{
foreach (string[] row in values)
{
float x = x0;
for (int col_num = 0; col_num < row.Length; col_num++)
{
// Prepare the StringFormat and drawing rectangle.
sf.Alignment = horz_alignments[col_num];
sf.LineAlignment = vert_alignments[col_num];
rect.X = x;
rect.Y = y0;
rect.Width = col_widths[col_num];
// Draw.
gr.DrawString(row[col_num], font, brush, rect, sf);
// Draw the box if desired.
if (draw_box) gr.DrawRectangle(box_pen,
rect.X, rect.Y, rect.Width, rect.Height);
// Move to the next column.
x += col_widths[col_num];
}
// Move to the next line.
y0 += row_height;
}
}
}
This method loops through the rows in the data and the columns in each row. For each data value, the code creates a RectangleF that is the maximum row height tall and that column's width wide. It sets a StringFormat object's Alignment and LineAlignment values to properly position the data and then draws it. If the draw_box parameter is true, the code also draws a box around the entry.
After drawing each entry in a row, the code advances the variable x to the position where it should draw the next entry. After it finishes a row, the code advances variable y0 so it is ready to draw the next row.
The final piece to the example is the PictureBox's Paint event handler shown in the following code.
// Display the data aligned in columns.
private void picColumns_Paint(object sender, PaintEventArgs e)
{
using (Font font = new Font("Times New Roman", 12))
{
// Get the row and column sizes.
float row_height;
float[] col_widths;
GetRowColumnSizes(e.Graphics, font, Values, out row_height, out col_widths);
// Add column margins.
const float margin = 10;
for (int i = 0; i < col_widths.Length; i++) col_widths[i] += margin;
// Draw.
e.Graphics.Clear(Color.White);
e.Graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
DrawTextRowsColumns(e.Graphics, font, Brushes.Black, Pens.Blue,
margin / 2, margin / 2, row_height, col_widths,
VertAlignments, HorzAlignments, Values, true);
}
}
This code calls the GetRowColumnSizes method to get the maximum row height and the column widths. It adds a margin to each column width so the results aren't too crowded.
Finally the code clears the PictureBox's background and calls the DrawTextRowsColumns method to draw the data.
The ListBox control's FormatString property determines how the control formats the values it displays. This can be particularly useful if you need to display values that need special formatting such as dates and currency amounts.
When it starts, this example uses the following code to display formatted values.
private void Form1_Load(object sender, EventArgs e)
{
double[] prices =
{
13.38, 7.75, 50.61, 532.21, 8.29, 111.11, 962.38,
49.27, 4.06, 98.45, 896.13, 7.51, 592.09, 238.29,
};
lstC.FormatString = "C";
lstC.RightToLeft = RightToLeft.Yes;
lstC.DataSource = prices;
DateTime[] dates =
{
new DateTime(2013, 4, 1),
new DateTime(2013, 3, 21),
new DateTime(2013, 7, 18),
new DateTime(2013, 9, 9),
new DateTime(2013, 11, 30),
new DateTime(2013, 2, 12),
new DateTime(2013, 4, 1),
new DateTime(2013, 3, 21),
new DateTime(2013, 7, 18),
new DateTime(2013, 9, 9),
new DateTime(2013, 11, 30),
new DateTime(2013, 2, 12),
};
lstNone.RightToLeft = RightToLeft.Yes;
lstNone.DataSource = dates;
lstD.FormatString = "D";
lstD.RightToLeft = RightToLeft.Yes;
lstD.DataSource = dates;
}
The code first create an array of doubles. It sets the lstC ListBox control's FormatString property to C so it displays the values as currency. The code also sets the control's RightToLeft property to Yes so the values are aligned on the right. It then sets the control's DataSource property to the array of values so the control displays them.
Next the program creates a list of dates. It sets the lstNone ListBox control's LeftToRight property to Yes and sets its DataSource property so the control displays the dates. The program doesn't set the control's FormatString property so it displays the values with their default formatting.
The program finishes by displaying the same dates but using the "D" long date format.