Summary
CP+ is a Visual Studio .NET Add-In for Code Project fans (better known as addicts.) It allows you to keep up to date on new articles and threads in the Lounge all from within your favourite development environment. You may even click an Article or Post which takes your fancy and have your web browser automatically load at the correct URL, nifty isn't it?
CP+ makes use of the Code Project Web Service as it's data source.
This article runs through the major components of the CP+ project and highlights
useful techniques and methods that I have found while developing this application. A further more in-depth article will also be written in the coming weeks.
Disclaimer
My first C++ app was Hello World. This is my second C++ app, so please, bear with me. There are some obvious problems with the app code which I am hoping you, C++ elite, can help me out on. Any tips, tactics and ideas are very welcome.
You may point and laugh at some of my VB-isms in C++ code, but you had better be sure you have some constructive criticism, rather than just
criticism.
Introduction
The application consists of three major parts. A MC++ class (CPBrief
), a C# Windows Forms user interface (a User Control, CPPlusUI
) and a C# ActiveX host control (CPPlusAddInUC
.)
The ActiveX host control is required to host your Add-In within VS.NET. It acts as a container for your User Control and presents it in a Tool Window Add-In. I did not write this code but rather modified some very useful sample Add-In code from the Visual Studio.NET Automation Samples page. I did attempt to use the VC++ code, but it was far too complex for my current C++ skills.
The C# Windows Forms user interface part consists of a Tab
control, two DataGrid
s, two Timer
controls and a few other minor Windows Forms controls. The Timers control the update process while the DataGrids display the Articles and Lounge Posts. The whole package is wrapped up as a User Control to make it easy to drop into the ActiveX host as well other applications in the future. Additionally a Windows From, Settings
, is included which allows the user to customise CP+.
The MC++ class is relatively simple, being my first C++ class ever, and contains the logic
necessary to retrieve data from the CP web service as well as to store and return the data locally. It uses a couple of the XML classes in the .NET Framework.
There are three rough aims to this little endeavor:
- Find out what all the fuss is about C++
- Provide a quick and available means of checking up on CP which appeals to developers
- and explore the cross-language and common data type capabilities of .NET
Essentially though I opened my big mouth and got involved in the Visual C++
.NET Competition. After some spurring along the lines of "Paul Watson wouldn't know a C++ compiler even if it threw him a pointer" I took up the challenge and, I hope, met it. I do not golf, but that jacket is damned sweet.
CPBrief - the MC++ Class
Class Members
The CPBrief
class contains the following class members.
Private members
static String* AppPath
:
AppPath
is initialised with Application::StartupPath
and is used to access the various files needed by CP+. When run as an Add-In this maps to your Common7\IDE folder in your VS.NET installation. I used the static
keyword as this member should be the same for all instances of the class.
DateTime NextUpdate
:
This holds the future date and time of the next update cycle for the instance of the class. I compare this against DateTime::Now
to ascertain whether to update or not. Each time the UpdateBriefs
method is run the UpdateFrequency
value is added onto DateTime::Now
and stored in this member.
String *BriefName
:
When an instance of this class is constructed this member is set to the value passed to the constructor. Essentially this holds the "english" name of the class instance which is then used in an XPath query to retrieve adn set the correct settings from and to cp+settings.xml.
String *BriefURL
:
BriefURL
holds the full URL of the web service method to call for this instance of the class. This member is set in the constructor and pulled from the settings file.
String *BriefFileName
:
This member holds the filename to which the class saves the XML retrieved from the web service and also the filename from which the DataSet
reads the XML. This member is set in the constructor and pulled from the settings file.
Public members
bool UpdateOnLoad
:
Holds the value which decides whether to contact the web service whenever CP+ loads or not.
int NumberOfBriefsToRetrieve
:
Contains the number of brief items (e.g. an Article or Lounge Post) to retrieve from the web service.
TimeSpan UpdateFrequency
:
Holds the value of how frequently to contact the web service and update the DataGrid
s. The TimeSpan
type passes very easily between C# and MC++ code.
Class Methods
The CPBrief
class consists of the following methods.
CPBrief(String* BriefID)
(constructor)
The constructor deals with initialising the class instance correctly. In C# you construct the class instance by calling private CPPlus.CPBrief cpArticles = new CPPlus.CPBrief("ArticleBrief");
. "ArticleBrief"
is placed in the BriefName
member and is used to construct the XPath query which retrieves the rest of the class instance settings from the cp+settings.xml file.
xdocSettings->Load (String::Concat(AppPath,"cp+settings.xml"));
BriefURL = ReturnNodeValue(xdocSettings,
String::Concat("settings/briefsettings[@id='",
BriefName,"']/wsurl"));
The constructor applies similar code to return all the settings for the instance.
SaveSettings
SaveSettings
essentially does the opposite of the constructor. It pulls the values from the class members and saves them back into the settings file. This method is only called onclose
of the Settings
Windows Form in the C# app.
Saving to XML is similar to writing. You create an XmlDocument
and then use it's SelectSingleNode
method to return a node reference. You then set the value
of that node equal to the setting. You then save the XmlDocument
.
I wrapped the actual node setting portion up in a custom function, SetNodeValue
, to make it easy to set node values when needed.
SetNodeValue(xdocSettings->SelectSingleNode(String::Concat(
"settings/briefsettings[@id='",BriefName,"']/updateonload")),
Convert::ToString(UpdateOnLoad));
...
void SetNodeValue(XmlNode* xnodeToSet, String* NodeValue)
{
...
xnodeToSet = xnodeToSet->FirstChild;
xnodeToSet->Value = NodeValue;
}
One thing to note is that the .NET XML classes treat an attribute
almost identically to an element
. This confused me at first, but actually made the code cleaner as I did not need an overload method to handle attributes
.
UpdateBriefs
Now we get to the actual web service portion. This method very simply reads XML from a specified URL and saves it to the BriefFileName
XML file. It does not use a web service proxy but rather the XmlTextReader
class which very handly can read from URLs over HTTP.
The reason for using this class and not a web service proxy is actually part of an ongoing debate between proponents of SOAP and REST. SOAP is the web service way and effectively using XmlTextReader
is the REST way. REST, in a nutshell, argues that we do not need further packet wrappers (like SOAP) to turn web servers into web service providers. For most cases using a QueryString in a URL will suffice in telling a web service what XML to return.
You can practice the REST way by typing http://www.codeproject.com/webservices/latest.asmx/GetLatestArticleBrief?NumArticles=2 into your URL bar. You have just used a web service. The return data is in XML format and can then be utilised as needed. With SOAP you would require some further methods and functions to get at the XML.
However in my opinion both SOAP and REST have their place. SOAP is better when you have complex web services which return typed XML. REST is good for the simpler stuff.
Another reason why I decided on REST is because all I wanted to do was to save the returned XML straight to a file. I did not need to perform any further operations on the XML.
Here is the code for this method:
xtrBriefs = new XmlTextReader (String::Concat(BriefURL,
NumberOfBriefsToRetrieve.ToString()));
xtwBriefs = new XmlTextWriter (String::Concat(AppPath,BriefFileName), 0);
xtwBriefs->Formatting = Formatting::Indented;
while (xtrBriefs->Read())
{
xtwBriefs->WriteRaw(xtrBriefs->ReadOuterXml());
}
xtwBriefs->Flush();
xtwBriefs->Close();
xtrBriefs->Close();
So simple it hurts. However I do want to make a few improvements on this code in the future as the error handling is virtually non-existent. If the CP web service happened to be down when this code is called the XmlTextReader
would return no data and that is what the XmlTextWriter
would write to the file, exactly no data.
ReturnBriefs
This method reads the written XML data into a DataSet
and presents the DataSet
for binding to the DataGrid
. Also very simple, very direct and once again not much error checking (a weak point if I ever had one and something which I hope you can help me out with.)
DataSet* ReturnBriefs()
{
DataSet* dsBriefs = 0;
dsBriefs = new DataSet();
try
{
dsBriefs->ReadXml(String::Concat(AppPath,BriefFileName));
return dsBriefs;
}
catch (Exception* e)
{
throw e;
return 0;
}
}
The "table" structure is inferred by the DataSet
class itself from the XML. You can however create your own XSD schema document and associate it with the DataSet
to provide strongly typed data types. For our purposes, inferrence is fine (though technically, we could contact the web service and download the WSDL document which contains schema information. We could then save that and have an automatically created schema.)
In CP+ this code is called in C# like so:dgArticles.SetDataBinding(cpArticles.ReturnBriefs(),"ArticleBrief");
Usage
The class is very easy to use. All you need to do is create an instance, e.g. private CPPlus.CPBrief cpLoungePosts = new CPPlus.CPBrief("MessageBrief");
and ensure the cp+settings.xml file with the relevent matching (matching to the BriefName/ID
) elements is present in the apps path. You then call the UpdateBriefs
method to retrieve the items and then the ReturnBriefs
method which returns a DataSet
which you can databind or utlisize as you wish.
In CP+ the class is instantiated twice, once for Articles and once for Lounge Posts, and remains in scope as long as CP+ is loaded.
CP+UI - the C# User Control & Add-In
The User Control is a simple collection of Windows Forms controls which call and display the CPBrief
class. I will not go much into this part of the app in this article (as this is a VC++ comp.)
Inherited DataGrid
One interesting aspect of the UI is that I have inherited and then overriden portions of the standard .NET DataGrid
control. The standard DataGrid
has no way of displaying the underlined blue column items that you can see in CP+.
To obtain that effect I had to override the already thrice overloaded paint
method of the standard DataGridTextBoxColumn
(which is a part of the DataGrid
):
public class DataGridColoredTextBoxColumn : DataGridTextBoxColumn
{
protected override void Paint(Graphics g,Rectangle Bounds,
CurrencyManager Source,int RowNum, Brush BackBrush ,
Brush ForeBrush ,bool AlignToRight)
{
BackBrush = Brushes.White;
g.FillRectangle(BackBrush, Bounds.X, Bounds.Y, Bounds.Width, Bounds.Height);
System.Drawing.Font font = new Font(System.Drawing.FontFamily.GenericSansSerif ,
(float)8.25, System.Drawing.FontStyle.Underline );
g.DrawString( GetColumnValueAtRow(Source, RowNum).ToString(), font ,Brushes.Blue,
Bounds.X ,Bounds.Y );
}
}
Eventually I will pluck up the courage, and knowledge, to do this in C++, but for now C# will suffice. Thanks go to Mazdak for his Changing the background color of cells in a DataGrid article which showed me how to do this. No other resource on the web was as understandable and useful on this matter.
One caveat about the DataGrid
functionality is that while it provides automatic column sorting, it does not sort numbers at all well. It treats them alphanumerically and therefore 11 will come before 8 in a sort, you have been warned (CP+ does not fix this issue as of yet, so sorting the Replies column will not always be very effective.)
Opening Browser Windows
Another relatively interesting part of the UI was how to open a web browser window and point it at the relevant URL:
System.Diagnostics.Process.Start("http://www.codeproject.com");
If it was any easier I probably would not have believed it. The hard bit funnily enough came from figuring out how to detect and use a mouseclick on the DataGrid
. The DataGrid
makes do with one set of mouse events for all it's sub-components. So whether you click a cell, a column header or the grid itself, one set of events is fired. This I found to be silly as usually unneccesary code had to be written to handle the mouse events. To detect which row was clicked and so launch the correct URL one has to use the HitTestInfo
and HitTestType
classes. Following is the code for this:
private void dgArticles_MouseDown(object sender,
System.Windows.Forms.MouseEventArgs e)
{
DataGrid dgSender = (DataGrid) sender;
System.Windows.Forms.DataGrid.HitTestInfo hti;
hti = dgSender.HitTest(e.X, e.Y);
switch (hti.Type)
{
case System.Windows.Forms.DataGrid.HitTestType.Cell :
dgArticles.CurrentCell = new DataGridCell(hti.Row, hti.Column);
dgArticles.Select(hti.Row);
if (dgArticles.CurrentCell.ColumnNumber == 1)
{
System.Diagnostics.Process.Start(dgArticles[hti.Row,0].ToString());
}
break;
}
}
Add-In Host
The final bit of interest in the UI section involves how the User Control is hosted within VS.NET as a Tool Window. As mentioned I used the automation samples as well as the VS.NET Add-In template wizard.
The wizard however only takes you so far and does not include functionality for generating a Tool Window. Following is the important code in creating this Tool Window and having it host the User Control (though technically the Tool Window hosts the Interop.VSUserControlHostLib
ActiveX control which in turn hosts your User Control):
public void OnConnection(object application,
Extensibility.ext_ConnectMode connectMode,
object addInInst, ref System.Array custom)
{
...
applicationObject = (_DTE)application;
addInInstance = (AddIn)addInInst;
windowToolWindow
= applicationObject.Windows.CreateToolWindow (addInInstance,
"VSUserControlHost.VSUserControlHostCtl", "CP+",
guidstr, ref objTemp);
windowToolWindow.Visible = true;
windowToolWindow.Width = 500;
windowToolWindow.Height = 300;
objControl = (VSUserControlHostLib.VSUserControlHostCtl)objTemp;
System.Reflection.Assembly asm = System.Reflection.Assembly.GetExecutingAssembly();
objControl.HostUserControl(asm.Location, "CPPlusAddIn.CPPlusAddInUC");
...
}
Usage
Becaues the UI of CP+ is wrapped in a User Control, you can take the assembly and drop it onto your Windows Forms as needed. In the future I hope to extend the User Control itself so that you can specify the tabs and columns in the DataGrids
. Really, you use the CPBrief class when you want to wrap your own UI with the CP+ functionality and you use the CP+UI User Control when all you want is to quickly drop the functionality of CPBrief into your app.
You can also very easily re-use the Add-In host code for your own Add-Ins, rather than having to run the wizard or use the automation samples.
CP+ - The Add-In App
The actual app itself is simple to run and use. On load of the app it reads the settings file and determines whether to update or simply return the brief items from the XML storage files. You can customise the settings to fit your needs by clicking the settings button. You can also force the app to update from the web service without waiting for the next cycle by clicking the two buttons at the bottom left, one for Articles and one for Lounge Posts.
Please however be careful with the settings as certain values will make the app fall over. If CP+ stops working, the best thing to do is to overwrite your copy of the cp+settings.xml file with the original provided with this article, those settings work.
As mentioned my error trapping and handling is not up to scratch and is something I am going to be working on for the next release of the app.
Installation
The easiest installation method is to simply download the demo code and run the msi installer. This will install and register the app. You then need to copy the cp+settings.xml file from the installed folder to the Common7\IDE folder under your VS.NET installatin folder. If you don't do this, the app will not work.
Alternatively you can compile the source code yourself and merge the ReCreateCommands.reg registery file.
Once installed open up VS.NET and click the Tools menu. A yellow smiley icon should be there, clicking this will open CP+ which you can then dock and auto-hide to your hearts content.
Notes & ToDo Features
- If you do do development using the User Control or MC++ class library I recommend you debug and test using a test Windows Form project, rather than debugging through an actuall Add-In. This just makes your life easier as you can simply hit F5 instead of having to load up another copy of VS.NET and running the Add-In
- While I was really impressed by the VS.NET IDE features, I was also rather unimpressed by it's stability, especially when using the Form Designers. Developing User Controls with inherited controls often generates IASync errors and the class library DLLs often get locked and can only be unlocked (for re-building) by closing down VS.NET
- Only use the
static
keyword on class members when you know what it does!
- Not all class usage works the same in C# as it does in C++. Case in point is the
Node
class which is abstract. In C# you can instantiate it fine as I would expect, but in C++ you cannot
I have a lot of ideas to further enhance this application.
- Implement Asynchronous calls on the Update functions so that the app does not "freeze" while the web service is being consumed (on fast connections you may not notice the lock-up, but I certainly do on my dial-up line)
- Highlight new items and retain old items. Currently the app simply overwrites the XML files with the new one from the web service
- Implement a SysTray indicator for when new Articles or Lounge Posts are detected (just like Outlook, only better of course)
- Handle HTML in Article Ticles, Authors Names and Post Subjects. I hope to use James' Elementary HTML Parser for this
- Add in a Favourites tab so that you can have a list of your favourite articles available from Visual Studio.NET. Similar to Code Projects Bookmark feature
- Implement a simple search
- Allow for "filtering" so that you do not get alerted of new articles or posts you are not interested in
- Validation on the settings to stop you typing in 999 articles
- Better error handling. At the moment if the web service is down and no XML is returned, the damned thing clears the Article and Lounge Post list, not helpful at all
- Find and use something better than the DataGrid to display the items, any ideas welcome
- Write the whole thing in MC++, just to prove I can :-D (though the Add-In portion looks like a complete nightmare if done using C++)
Of course the app can only go so far as the data provided by the web service. I hope to see Chris expanding the web service to other forums and also providing extra info on the Articles so more advanced filtering and selection can be performed. Saying that though, this web service as it is is fantastic and more proof to me that web service's really are a good thing. Imagine having to screen-scrape all this info?
Conclusion
Naturally the actual C++ portion of this app and article will be fairly simple to most C++ coders. However it is my first steps and I hope to go further. I have caught some glimpses of why "you lot" rave about C++. Probably the best glimpse I have seen so far is realising you can bend it like Beckham in C++ what you could not even dribble like Tony Blaire in C#. However that bending of the rules and no report back on when it goes pear shaped can be very frustrating when you do not know what has gone wrong.
I have a lot to learn and I hope through this article I have illustrated what I need to learn and maybe you can provide some pointers (haha, I made a C++ joke :rolleyes:).
I was also impressed by how easy it is to use different languages in the same app using .NET. I simply had two projects, one C# and the other MC++. The C# project then simply added the MC++ project as a
dependency to get at it's classes.
So now we have the Code Project Screensavers, Rama's Ticker and my humble CP+ app all to keep us up to date as easily as possible with our favourite resource, The Code Project.
Thanks to, naturally, Chris for religiously spurring me on with this competition and also to the rest of you for providing the fuel that lit the fire :-D
I hope you find the app useful and I will see you later and you can admire my sweet new yellow jacket ;)