Google
WWW Yariv Hammer's Code Site

Monday, November 28, 2005

DataGrid With Columns of Any Control You Like

Introduction
The DataGrid control in VS.NET 2003 only has two types of columns: TextBox and CheckBox.
It is often asked how can one put other controls inside the DataGrid (Like pictures, links, ComboBoxes, or even user controls). The answer is not very comforting. In order to achieve such an effect you will need to implement your own DataGridColumnStyle. This can be quite hard. In this article I will show how to implement a generic base class that can support a lot of controls. It will be demonstrated with a ComboBox column style. You will then be able to do modifications to the code to suit your needs.

Creating a DataGridGenericColumn
The DataGridGenericColumn will be the base class of all Column Styles.
---------------------------------------------------

public class DataGridGenericColumn:DataGridTextBoxColumn
{
private DataGridGenericColumn()
{}
public DataGridGenericColumn(Control control)
{
_control= control;
_control.Leave += new System.EventHandler(control_Leave);
_control.TextChanged += new EventHandler(control_TextChanged);
}
private Control _control;
private bool _isEditing = false;
protected Control Control
{
get { return _control; }
}
private void control_Leave(object sender, EventArgs e)
{
_control.Hide();
}
private void control_TextChanged(object sender, EventArgs e)
{
_isEditing = true;
base.ColumnStartedEditing((Control) sender);
}


protected override void Edit(CurrencyManager source, int rowNum, System.Drawing.Rectangle bounds, bool readOnly, string instantText, bool cellIsVisible)
{
base.Edit(source,rowNum,bounds,readOnly,instantText,cellIsVisible);
ShowControl();
}
protected virtual void ShowControl()
{
_control.Parent = this.TextBox.Parent;
_control.Location = this.TextBox.Location;
_control.Size = new Size(this.TextBox.Size.Width, this.TextBox.Size.Height);
_control.Text = this.TextBox.Text;
this.TextBox.Visible = false;
_control.Visible = true;
_control.BringToFront();
_control.Focus();
}
protected override bool Commit(CurrencyManager dataSource, int rowNum)
{
if (_isEditing)
{
_isEditing = false;
SetColumnValueAtRow(dataSource,rowNum,GetValue());
}
return true;
}
protected virtual object GetValue()
{
return _control.Text;
}
}
--------------------------------------------------
The base class is DataGridTextBoxColumn. It will be more convinient to inherit from this class than from DataGridColumnStyle, because a lot is already implemented for us. The base class has a TextBox property, which represents the DataGridTextBox that is shown in the cell when the user edit the cell.
In the constructor you pass a Control. Any Control will do. This Control is stored in a private member, and there is a protected member Control which may be accessed in the derived classes.
The Edit method is overrided in our class. The whole trick of showing our control instead of the TextBox is implemented in the ShowControl method. We set the location and size of our control to be the same as the TextBox, and then set the Visible property of our control to true, while setting the it to false form the TextBox.
Whenever the Text is changed inside the control, we remember this in _isEditing member. When the TextChanged event is raised, we know that we will need to store the new value into the DataSource. This is done in the Commit method, which we overrided from the base class. SetColumnValueAtRow method is used here in order to commit the changes into the DataSource. This method is implemented for us in the base class. The value is received from the virtual method GetValue. For our generic control we use the Text property, but this might not be the case for other controls.
Finally the Leave event of our control will be the trigger for making it invisible.
Note: You might want to make the control visible all the time (for example in case of pictures). It should be possible.

Creating a Column of ComboBoxes
Next, we need to derive from our new DataGridGenericColumn. We will implement a column of ComboBoxes.
-------------------------------------------------------

public class DataGridComboBoxColumn:DataGridGenericColumn
{
public DataGridComboBoxColumn():base(new DataGridComboBox())
{
}
public DataGridComboBox Combo
{
get { return (DataGridComboBox) Control; }
}
public class DataGridComboBox:ComboBox
{
private const int WM_KEYUP = 0x101;
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_KEYUP) return;
base.WndProc(ref m);
}
}
}
-------------------------------------------------------
The code is surprisingly very short - only a constructor that passes to the base class a new DataGridComboBox. Also there is a public property Combo to expose the control to the outside world. It is important as we will see in the demo soon.
The DataGridComboBox is implemented only because I wanted to fix a bug regarding the Tab key. IF you fill uncomfortable with it, then be my guest to use a normal ComboBox (It will work except for the small bug). I won't get into specific details here.

Usage of this control is as follows:
-------------------------------------------------------
DataGridComboBoxColumn cs4 = new DataGridComboBoxColumn();
cs4.MappingName = "ColumnName";
cs4.HeaderText = "Title";
cs4.Combo.Items.AddRange(new string[] {"a","b","c"});
-------------------------------------------------------
As long as the Items cllection has objects with ToString implemented for them, the combo will work great (for columns of type string of course).

Implementing a Data Bound ComboBox Column
Next, I will derive from our new DataGridComboBoxColumn and create a Data Bound ComboBox column. This means that you will be able to set a DataSource, Display and Value members for the ComboBox, and allow you to show a list from other tables (for example in a case of foreign key).
-------------------------------------------------------

public class DataGridDataBoundComboBoxColumn:DataGridComboBoxColumn
{
private DataGridDataBoundComboBoxColumn()
{
}
public DataGridDataBoundComboBoxColumn(object dataSource,string displayMember, string valueMember):base()
{
Combo.DataSource = dataSource;
Combo.ValueMember = valueMember;
Combo.DisplayMember = displayMember;
}
protected override object GetValue()
{
if (Combo.SelectedValue == null)
return DBNull.Value;
return Combo.SelectedValue;
}
protected override void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum, Brush backBrush, Brush foreBrush, bool alignToRight)
{
string text = GetText(GetColumnValueAtRow(source, rowNum));
PaintText(g, bounds, text, backBrush, foreBrush, alignToRight);
}
protected virtual string GetText(object value)
{
if (value == DBNull.Value)
return NullText;
if (value != null)
{
DataRow[] dr = ((DataView)Combo.DataSource).Table.Select(Combo.ValueMember + " = " + value.ToString());
return dr[0][Combo.DisplayMember].ToString();
}
else
return "";
}
}
-------------------------------------------------------
The constructor receives a DataSource (DataView, DataTable, or any other legitimate data source), a DisplayMember (name of the column you want to display, like Name), and a ValueMember (name of the columns you want as value, it can be the same, or it can be the Id column).
The GetValue method we have implemented before was overriden, because we need the SelectedValue property (Text will not be enough).
We override the Paint method, because we want to show in the cell the Display text instead of the value. The GetText method is important to show the correct text in the cell when it is not edited. It will work if the data source of the ComboBox is a DataView or a DataTable (You may modify otherwise).

Demo of The Custom ColumnStyles
Start a new Project. Place the above code in the project (or reference to it).
Drag two DataGrids to the form (dataGrid1 above dataGrid2). Double click on it, and place the following code inside the form load event handler:
---------------------------------------------

private void Form1_Load(object sender, System.EventArgs e)
{
DataTable dtData = new DataTable("Data");
dtData.Columns.Add(new DataColumn("Id",typeof(long)));
dtData.Columns.Add(new DataColumn("Name",typeof(string)));
dtData.Columns.Add(new DataColumn("Foreign",typeof(long)));
dtData.Columns.Add(new DataColumn("List",typeof(string)));

DataTable dtConst = new DataTable("Constants");
dtConst.Columns.Add(new DataColumn("Value",typeof(long)));
dtConst.Columns.Add(new DataColumn("Text",typeof(string)));
dataGrid2.DataSource = new DataView(dtConst);

dataGrid1.DataSource = new DataView(dtData);
DataGridTableStyle ts = new DataGridTableStyle();
ts.MappingName = "Data";
DataGridTextBoxColumn cs1 = new DataGridTextBoxColumn();
cs1.MappingName = "Id";
cs1.HeaderText = "Id";

DataGridTextBoxColumn cs2 = new DataGridTextBoxColumn();
cs2.MappingName = "Name";
cs2.HeaderText = "Name";

DataGridDataBoundComboBoxColumn cs3 = new DataGridDataBoundComboBoxColumn(new DataView(dtConst),"Text","Value");
cs3.MappingName = "Foreign";
cs3.HeaderText = "Foreign";

DataGridComboBoxColumn cs4 = new DataGridComboBoxColumn();
cs4.MappingName = "List";
cs4.HeaderText = "List";
cs4.Combo.Items.AddRange(new string[] {"a","b","c"});

ts.GridColumnStyles.AddRange(new DataGridColumnStyle[] {cs1,cs2,cs3,cs4});
dataGrid1.TableStyles.Add(ts);
}
---------------------------------------------
Run the program. As you can see if you add data in the bottom DataGrid (rows with integer values and text fields), you will be able to select those when you click in cells in the Foreign column in the top grid. In the List column you can select one of the constant values.

Saturday, November 12, 2005

Creating a Broadcast Events Server Using .NET Remoting - A Step-By-Step Tutorial

Introduction
It is possible to use events in Remoting. This means that the server can raise events in the clients, which means that client don't need to use timers in order to check if something happened in the server. This is a very important capability of .NET Remoting.
However, it is not such an easy job using remote events, as you would imagine. This is a tutorial which will help you create your events. Simply follow it step by step, and customise it for your needs.
The demo is in C#.
Download Full Source Code (<100kb)

Step 1 - Setup the solution
Create an empty solution. Add three projects to it:


  • Server - A windows application.
  • Client - A Windows application.
  • Common - A Class library
Add a reference from the Client and the Server to the Common library.
Remove all forms and classes autogenerated for you when you set up the new projects.

Step 2 - Create a Message Class and IBroadcaster interface
In Common library, add a new class called Message.
Here is the code:
-----------------------------------

using System;
namespace Common
{
[Serializable()]
public class Message
{
public string _msg;
public Message(string msg)
{
this._msg = msg;
}


public string Text
{
get { return _msg; }
}

}
}

--------------------------------------------
Note the Serializable attribute. The Message is going to cross processes between the clients and the server.

Next add a new class to Common. called IBroadcater. We will change it an interface:
--------------------------------------------
namespace Common
{
public delegate void MessageArrivedHandler(Message msg);

public interface IBroadcaster
{
void BroadcastMessage(Message msg); // Send message
event MessageArrivedHandler MessageArrived; // Receive message
}
}

--------------------------------------------
We have just defined a delegate to use as our event, and n interface which will handle both sending the message and receiving it.
Notice that I put an interface in Common. This way the client will only be able to access the broadcaster using the interface, without having access to the actual implementaion.

Step 3 - Create the Broadcaster Class
Now add a class to the Server project. Call it Broadcaster.
Here is the code:
-------------------------------------------

using System;
using Common;
namespace Server
{
[Serializable()]
public class Broadcaster:MarshalByRefObject,IBroadcaster
{

public override InitializeLifetimeService()
{//Add this to the download source code.
return null;
}
public event MessageArrivedHandler MessageArrived;
public void BroadcastMessage(Message msg)
{
SafeInvokeEvent(msg);
}
private void SafeInvokeEvent(Message msg)
{
if (MessageArrived == null)
return;
MessageArrivedHandler mah=null;
foreach(Delegate del in MessageArrived.GetInvocationList())
{
try
{
mah = (MessageArrivedHandler)del;
mah(msg);
}
catch (Exception ex)
{
Console.Write(ex.Message);
MessageArrived -= mah;
}
}
}
}
}

----------------------------------------------------
First of all, credits for this class must go to Ingo Rammar's book "Advanced .NET Remoting".
This class is very important to understand. Notice that the class derives from MarshalByRefObject. This means we are going to register it for remoting - when clients will call it, they will use a proxy, and the actual call will be done in the Server process. Next notice that we implement the IBroadcaster interface. This will alow it to access the class from the clients (The inetrface resides in the Common interface which is refernced by the client).
The main thing to notice is the SafeInvokeEvent method. Intead of just raising the event like I would normally do, I iterate through all the delegates registered to the event (these are all remote events!!), and invoke them one by one. If for any reason the invoke is failed, I remove the delegate rom the InvokationList. This is super important. Without it when a client will disconnect I will not be able to raise the event again!!!

Step 4 - Setup Remoting in the Server
Add a new item to the Server - An Application Configuration File (App.Config)
-----------------------------------------------


<configuration>
<system.runtime.remoting>
<application name="EventServer">
<service>
<wellknown mode="Singleton" type="Server.Broadcaster, Server" objectUri="Broadcaster.soap"/>
</service>
<channels>
<channel ref="tcp" port="16784">
<serverProviders>
<formatter ref="binary" typeFilterLevel="Full"/>
</serverProviders>
<clientProviders>
<formatter ref="binary"/>
</clientProviders>
</channel>
</channels>
</application>
</system.runtime.remoting>
</configuration>

----------------------------------------------------------
The name of server is configurable (remember it for the client's configuration file). Same for the port, and the ObjectURI.
The type must contain the full name of the object (containing namespace), and the server assembly name.

Add a Form to the Server called ServerForm. No code needed yet.
Add a class to the server called ServerStartup:
----------------------------------------------------------

using System.Windows.Forms;
namespace Server
{
public class ServerStartup
{
[STAThread]
static void Main()
{
bool createdNew;
Mutex m = new Mutex(true, "EventServer", out createdNew);
if (!createdNew)
{
return;
}
InitRemoting();
Application.Run(new ServerForm());
GC.KeepAlive(m);
}
private static void InitRemoting()
{
string fileName = "server.exe.config";
RemotingConfiguration.Configure(fileName);
}
}
}

-------------------------------------------------------
Few things to notice here: The Main method forces this application to be single instance (Check this post). The InitRemoting method reads the App.Config file and setup the Broadcaster class for remoting use.

Step 5- Creating EventHelper
Add a class to Common called BroadcastEventHelper:
-------------------------------------------------------

using System;
namespace Common
{
public interface IEventHelper
{
event MessageArrivedHandler MessageArrivedLocally;
void LocallyHandleMessageArrived(Message msg);
}
[Serializable()]
public class BroadcastEventHelper:MarshalByRefObject,IEventHelper
{
public event MessageArrivedHandler MessageArrivedLocally;
public void LocallyHandleMessageArrived(Message msg)
{
if (MessageArrivedLocally != null)
MessageArrivedLocally(msg);
}
public override object InitializeLifetimeService()
{
return null;
}
}
}

-------------------------------------------------------------------
First we define IEventHelper interface, which has an event and a method to raise the event.
The class of BroadcastEventHelper is going to be used in the client process (not the server) in a way I will show and explain in the next step. It may seem unnessecary to you now, but actually it is very important. It implements the IEventHelper interface as well as inherits from MarshalByRefObject.

Step 6 - Creating MessageBroadcaster in the Client
First we need to add a class to Common, called RemotingHelper:
-------------------------------------------------------------------

using System;
using System.Collections;
using System.Runtime.Remoting;
namespace Common
{
public class RemotingHelper
{
internal static bool _isInit = false;
private static IDictionary _wellKnownTypes;
public static object GetObject(Type type)
{
if (!_isInit)
InitTypeCache();
WellKnownClientTypeEntry entr = (WellKnownClientTypeEntry) _wellKnownTypes[type];
if (entr==null)
throw new RemotingException("Type not found");
return Activator.GetObject(entr.ObjectType,entr.ObjectUrl);
}
private static void InitTypeCache()
{
_wellKnownTypes = new Hashtable();
foreach(WellKnownClientTypeEntry entr in RemotingConfiguration. GetRegisteredWellKnownClientTypes())
_wellKnownTypes.Add(entr.ObjectType,entr);
_isInit = true;
}
}
}

------------------------------------------------------
This class will help us to get the Singleton instances of the Remote objects generically. (For further details check this post).

We will add a special Exception called ServerNotAliveException, so we add a file called Exceptions to common, and add this code:
------------------------------------------------------

using System;
namespace Common
{
public class ServerNotAliveException:Exception
{
public override string Message
{
get
{
return "Server is not connected!";
}
}
}
}

-------------------------------------------------------

Now we add a class to the Client project called MessageBroadcaster:
-------------------------------------------------------

using System;
using System.Net.Sockets;
using System.Threading;
using Common;
namespace Client
{
public class MessageBroadcaster:IBroadcaster
{
private IBroadcaster _bcaster = null;
private BroadcastEventHelper _eventHelper = null;
public MessageBroadcaster()
{

InitializeObjects();
}
public void BroadcastMessage(Message msg)
{
if (_bcaster == null)
throw new ServerNotAliveException();
try
{
_bcaster.BroadcastMessage(msg);
}
catch (SocketException ex)
{
Console.WriteLine(ex.Message);
}
catch (ThreadAbortException ex)
{
Console.WriteLine(ex.Message);
}
}
private void HandleMessage(Message msg)
{
if (MessageArrived != null)
MessageArrived(msg);
}
private void InitializeObjects()
{
_bcaster = (IBroadcaster)RemotingHelper.GetObject(typeof(IBroadcaster));
_eventHelper = new BroadcastEventHelper();
_eventHelper.MessageArrivedLocally += new MessageArrivedHandler(HandleMessage);
_bcaster.MessageArrived += new MessageArrivedHandler(_eventHelper.LocallyHandleMessageArrived);
}
public event MessageArrivedHandler MessageArrived;
}
}

-----------------------------------------------------
_bcaster is a member of typed IBroadcaster, but actually it is the Broadcaster object in the server. _eventHelper is the BroadcastEventHelper from the previous step, and is in the client process. We use _bcaster in order to Broadcast an event (sending it to all the connecting clients) by calling BroadcastMessage. We sign up the MessageArrived event of the server object with the delegate from BroadcastEventHelper and we sign up for the MessageArrivedLocally. Here is what's going on: If a client raises an event it calls BroadcastMessage in the Server Broadcaster. The server uses the SafeInvokeEvent method and raises the event in all the clients. The client receives the event in the BroadcasterEventHelper, and the method LocallyHandleMessageArrived is called. The method raises another event, MessageArrivedLocally , which is now in the client process entirely. The MessageBroadcaster method HandleMethod is then called and raises the event MessageArrived. Until now everything was generic, and the client will use the MessageBroadcaster objects, and listen to this event, without being concerned with this entire process.
Why do we need all that complexity? When the client raises an event in the server they actually swap roles: The Server becomes the client of the event raised by the Client (Which becomes the server). In order for that to work the server must have a reference to the MarshalByRefObject in the client that raised the event. This cannot be: The server cannot have a reference to client assembly, and we can also have a circular referencing problem. That is why we have the BroadcastEventHelper. This class does nothing beside handle the events (no specific logic or data), and it resides in the server process. The client and the server reference both to the Common assembly so our problem is solved. The only minus is that we need to have another wrapper for the event in the client process.

Step 7 - Setup Remoting in the Client
Our final step will be to add an App.Config file to the Client:
------------------------------------------------------------------
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application name="EventClient.exe">
<client>
<wellknown type="Common.IBroadcaster, Common" url="tcp://localhost:16784/EventServer/ Broadcaster.soap"/>
</client>
<channels>
<channel ref="tcp" port="0">
<serverProviders>
<formatter ref="binary" typeFilterLevel="Full"/>
</serverProviders>
<clientProviders>
<formatter ref="binary"/>
</clientProviders>
</channel>
</channels>
</application>
</system.runtime.remoting>
</configuration>

------------------------------------------------------------------
We register the IBroadcaster interface for remoting (instead of the concrete class) and the URI tells what class we mean. You can replace the IP address, port, and name of the server application (look at the server configuration file).

Now we add another Form called ClientForm.
We add a class called ClientStartup to the Client:
------------------------------------------------------------------

using System;
using System.Runtime.Remoting;
using System.Windows.Forms;
namespace Client
{
public class ClientStartup
{
[STAThread]
static void Main()
{
InitRemoting();
Application.Run(new ClientForm());
}
private static void InitRemoting()
{
string fileName = "client.exe.config";
RemotingConfiguration.Configure(fileName);
}
}
}

------------------------------------------------------------------

Now a demo to see how it works.
Demo
In the ClientForm drag a ListBox, and call it lsbMessages, and a Button called btnSendMessage with text "Send Message". Double-click on the Button, and add this code to the form:
-----------------------------------------------------------------

using System.Threading;
using Common;

namespace Client
{
public class ClientForm : System.Windows.Forms.Form
{
(...)

public ClientForm()
{
InitializeComponent();
_messageBroadcaster.MessageArrived += new Common.MessageArrivedHandler(broadcaster_MessageArrived);
}
(...)

MessageBroadcaster _messageBroadcaster = new MessageBroadcaster();
private long _index=0;
private void btnSendMessage_Click(object sender, System.EventArgs e)
{
Thread t = new Thread(new ThreadStart(SendMessage));
t.Start();
}
private void SendMessage()
{
try
{
Message msg = new Message("This is message #" + (_index++).ToString());
_messageBroadcaster.BroadcastMessage(msg);
msg = null;
}
catch (ServerNotAliveException ex)
{
lsbMessages.Items.Add(ex.Message);
lsbMessages.SelectedIndex = lsbMessages.Items.Count-1;
}
}
private void broadcaster_MessageArrived(Message msg)
{
lsbMessages.Items.Add(msg.Text);
lsbMessages.SelectedIndex = lsbMessages.Items.Count-1;
}
}
}

-----------------------------------------------------------------
Note how we handle the MessageArrived event (by adding the message to the ListBox). The reason I used a Thread to croadcast the event, was in order not to block the UI when calling the server. Keep in mind this is just an example.

You can now run the server, and as many clients as you want, press the buttons and see how all the clients receive the event.

Lets improve the Server:
Add the class RemoteSingletonObjectsList to the server
-----------------------------------------------------------------
using System;
using System.Collections;
namespace Server
{
public class RemoteSingletonObjectsList
{
private static RemoteSingletonObjectsList _instance = new RemoteSingletonObjectsList();
public static RemoteSingletonObjectsList Instance
{
get
{
return _instance;
}
}
private RemoteSingletonObjectsList()
{
}
private Hashtable _objects = new Hashtable();
public void RegisterRemoteSingletonObject(object obj)
{
_objects[obj.GetType()] = obj;
}
public object GetRemoteSingletonObject(Type type)
{
return _objects[type];
}
}
}

------------------------------------------------------
This class will keep the singleton instances of the remote objects, thatway we access the same instances the client see from the server process.
Go to ServerStartup class and change a bit the InitRemoting method:
-----------------------------------------------------

private static void InitRemoting()
{
string fileName = "server.exe.config";
RemotingConfiguration.Configure(fileName);
foreach (WellKnownServiceTypeEntry entry in RemotingConfiguration. GetRegisteredWellKnownServiceTypes())
{
MarshalByRefObject pxy = (MarshalByRefObject) Activator.GetObject(entry.ObjectType, "tcp://localhost:16784/EventServer/" + entry.ObjectUri);
pxy.CreateObjRef(entry.ObjectType);
RemoteSingletonObjectsList.Instance. RegisterRemoteSingletonObject(pxy);
}
}

-----------------------------------------------------------
Now add to the ServerForm a ListBox (lsbMessages) and a button (btnSend with text "Send Message"). Double click on the button and add the code:
-----------------------------------------------------------

using System.Threading;
using Common;
using Message = Common.Message;

namespace Server
{
public class ServerForm : Form
{
(...)

public ServerForm()
{
InitializeComponent();
_bcast = (Broadcaster) RemoteSingletonObjectsList.Instance. GetRemoteSingletonObject(typeof(Broadcaster));
_bcast.MessageArrived += new MessageArrivedHandler(bcast_MessageArrived);
}
(...)

private Broadcaster _bcast = null;
public void bcast_MessageArrived(Message msg)
{
_msgToShow = msg;
Thread t = new Thread(new ThreadStart(ShowMessage));
t.Start();
}
private Message _msgToShow=null;
private void ShowMessage()
{
if (_msgToShow == null) return;
lsbMessages.Items.Add(_msgToShow.Text);
lsbMessages.SelectedIndex = lsbMessages.Items.Count -1;
_msgToShow = null;
}
private long index=0;
private void btnSend_Click(object sender, System.EventArgs e)
{
_bcast.BroadcastMessage(new Message("Server: This is message #" + (index++).ToString()) );
}

}
}

-----------------------------------------------------------
Now run the server and a few clients and press on buttons in the clients and server and see the events coming.

In the next post I will show how you can notify the clients and the server when there is a disconnection, and let them recover such disconnections.

Feel free to use everything here. Add links to my site if you wish.

Do not copy anything to other sites without adding link to here.

All the contents of the site belong to Yariv Hammer.