|
|||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Introduction.NET 2.0 introduces a lot of new features in Windows Forms and deployment technologies that take Smart Client development to the next level. Previously, we had to worry about too many issues including rich controls (owner drawn controls), multithreading, auto update and various other issues while developing a Smart Client. All these have been taken care of in the 2.0 Framework. However, most of the web based systems including Web Services and Web Sites are already developed in .NET 1.1. So, in this article, we will look at a real life scenario where a .NET 2.0 based Smart Client will be using a well engineered Service Oriented Architecture (SOA) based web service collection developed using .NET 1.1. The sample application will show you from early design issues to the deployment complexities, and how you can make a Smart Client smartly talk to a collection of web services which are also well engineered using the famous SOA. The server side will be fully utilizing Enterprise Library June 2005 by using all of its application blocks. The resultant product is an ideal example of best practices in cutting edge technologies and latest architecture design trends. If you would like a pure .NET 1.1 Smart Client, please see my other article: RSS Feed Aggregator and Blogging Smart Client. Revision
Feature WalkthroughLogin
Loading Required Data Asynchronously
Tabbed Interface: Document-centric View
Owner-drawn Tree View
Offline Capability
Going Online
Being Smart ClientRequirementsIn order to qualify an application as a Smart Client, the application needs to fulfill the following requirements according to the definition of Smart Client at MSDN: Local Resources and User ExperienceAll Smart Client applications share is an ability to exploit local resources such as hardware for storage, processing or data capture such as compact flash memory, CPUs and scanners for example. Smart Client solutions offer hi-fidelity end-user experiences by taking full advantage of all that the Microsoft® Windows® platform has to offer. Examples of well known Smart Client applications are Word, Excel, MS Money, and even PC games such as Half-Life 2. Unlike "browser-based" applications such as Amazon.Com or eBay.com, Smart Client applications live on your PC, laptop, Tablet PC, or smart device. ConnectedSmart Client applications are able to readily connect to and exchange data with systems across the enterprise or the internet. Web services allow Smart Client solutions to utilize industry standard protocols such as XML, HTTP and SOAP to exchange information with any type of remote system. Offline CapableSmart Client applications work whether connected to the Internet or not. Microsoft® Money and Microsoft® Outlook are two great examples. Smart Clients can take advantage of local caching and processing to enable operations during periods of no network connectivity or intermittent network connectivity. Offline capabilities are not only of use in mobile scenarios however, desktop solutions can take advantage of offline architecture to update backend systems on background threads, thus keeping the user interface responsive and improving the overall end-user experience. This architecture can also provide cost and performance benefits since the user interface need not be shuttled to the Smart Client from a server. Since Smart Clients can exchange just the data needed with other systems in the background, reductions in the volume of data exchanged with other systems are realized (even on hard-wired client systems, this bandwidth reduction can realize huge benefits). This in turn increases the responsiveness of the user interface (UI) since the UI is not rendered by a remote system. Intelligent Deployment and UpdateIn the past, traditional client applications have been difficult to deploy and update. It was not uncommon to install one application only to have it break another. Issues such as "DLL Hell" made installing and maintaining client applications difficult and frustrating. The Updater Application Block for .NET from the Patterns and Practices team provides prescriptive guidance to those that wish to create self-updating .NET Framework-based applications that are to be deployed across multiple desktops. The release of Visual Studio 2005 and the .NET Framework 2.0 will beckon a new era of simplified Smart Client deployment and updating with the release of a new deploy and update technology known as ClickOnce. (The above text is copied and shortened from the MSDN site.) How is This a Smart Client
The ClientMicrosoft-style Automation ModelThe greatest feature of this application is that, it provides you with an automation model similar to what you see in Microsoft Office applications. As a result, you can write scripts to automate the application and you can also develop custom plug-ins. The idea and implementation of this architecture is pretty big and explained in this article: The basic idea is to make an observable Object Model that all the UI modules or Views subscribe to in order to provide services based on changes made to the object model. For example, as soon as you add a new
This is the object model of the Client application. public abstract class ItemBase : IDisposable, IXml
{
...
...
public virtual event ItemSelectHandler OnSelect;
public virtual event ItemQueryClose OnQueryClose;
/// <summary>
/// Event to notify that the item is changed
/// </summary>
public virtual event ItemChangeHandler OnChange;
/// <summary>
/// Event to notify observers to detach from the item
/// </summary>
public virtual event ItemDetachHandler OnDetach;
/// <summary>
/// Show the item in UI
/// </summary>
public virtual event ItemShowHandler OnShow;
}
When you extend your object from
So, this gives you the following features:-
So, you can now have objects which can broadcast events. Any module can anytime subscribe/unsubscribe to a student object and provide some service without ever knowing existences of the other modules. This makes your program's architecture extremely simple and loosely coupled as you only have to think about how to listen to changes in the global object model and how to update that object model. You never think, hmm... there's a title bar which needs to be changed when user types something in the "First Name" text box which is inside a Instead of doing these, what you can do is, make the Let's see the difference between how we normally design desktop applications and how an automation model can dramatically change the way we develop them:
Making such an object model is quite easy when you have the public class StudentModel : ItemBase
{
public StudentModel( Student profile, object parent ) : base( parent )
{
this.Profile = profile;
profile.AfterChanged += new StudentEventHandler(profile_AfterChanged);
}
void profile_AfterChanged(object sender, StudentEventArgs e)
{
base.IsDirty = true;
base.NotifyChange("Dirty");
}
}
Similarly, there is a
The second feature is the most useful one. If you have a public abstract class CollectionBase : System.Collections.CollectionBase
{
public event ItemChangeHandler OnItemChange;
public event ItemDetachHandler OnItemDetach;
public event ItemShowHandler OnItemShow;
public event ItemSelectHandler OnItemSelect;
public event ItemUndoStateHandler OnItemUndo;
public event ItemRedoStateHandler OnItemRedo;
public event ItemBeginUpdateHandler OnItemBeginUpdate;
public event ItemEndUpdateHandler OnItemEndUpdate;
public event CollectionAddHandler OnItemCollectionAdd;
public event CollectionRemoveHandler OnItemCollectionRemove;
public event CollectionClearHandler OnItemCollectionClear;
public event SelectionChangeHandler OnItemCollectionSelectionChanged;
public event CollectionAddRangeHandler OnAddRange;
public event CollectionRefreshHandler OnRefresh;
}
Command PatternFor desktop applications, this is probably the best pattern of all. Command Pattern has many variants, but the simplest form looks like this:
I have used Command Pattern for all activities that involve Web Service call and UI update. Here is a list of commands that I have used which will give you ideas how to break your code into small commands:
So, you see, all the user activities are translated to some form of commands. Commands are like small packets of instructions. Normally each command is self-sufficient. It does all the activities like calling web service, fetching data, updating object model and optionally updating the UI. This makes the commands truly reusable and very easy to call from anywhere you like. It also reduces duplicate codes in different modules which provide similar functionality. For details about Command pattern, see my famous article: Background Send Receive of TasksWe will now take a look at a complete implementation of everything we have seen so far. Let's look at the complicated module "Send Receive" which performs the synchronization.
There are two classes that host this view:
The public class SendReceiveDocument : DocumentBase
{
private IList<StudentModel> _Students;
public IList<StudentModel> Students
{
get { return _Students; }
set { _Students = value; }
}
public SendReceiveDocument(IList<StudentModel> students, object parent)
: base("SendReceive", "Send/Receive", new SendReceiveUI(), parent)
{
this._Students = students;
}
public override bool Accept(object data)
{
if (data is IList<StudentModel>)
return base.UI.AcceptData(data);
else
return false;
}
public override bool CanAccept(object data)
{
return false;
}
public override bool IsFeatureSupported(object feature)
{
return false;
}
}
It contains a list of public partial class SendReceiveUI : DocumentUIBase
{
/// <summary>
/// Return the students list from the document
/// </summary>
private IList<StudentModel> _Students
{
get { return (base.Document as SendReceiveDocument).Students; }
set { (base.Document as SendReceiveDocument).Students = value; }
}
public SendReceiveUI()
{
InitializeComponent();
}
internal override bool AcceptData(object data)
{
this._Students = data as IList<StudentModel>;
this.StartSendReceive();
return true;
}
}
So, the architecture looks like this:
When the synchronization starts, private void StartSendReceive()
{
// Populate the listview with students
this.tasksListView.Items.Clear();
foreach (StudentModel student in this._Students)
{
ListViewItem item = new StudentListViewItem(student);
this.tasksListView.Items.Add(item);
}
...
...
// Start background synchronization
worker.RunWorkerAsync();
}
Here
Here's the private void worker_DoWork(object sender, DoWorkEventArgs e)
{
// This method will run on a thread other than the UI thread.
// Be sure not to manipulate any Windows Forms controls created
// on the UI thread from this method.
this.BackgroundThreadWork();
}
I generally follow a practice to include the "Background" word in the methods which are called from here. This gives me a visual indication to remember that I am not in the UI thread. Also, any other function called from such a function are also marked with the "Background" word. The actual work is performed in this method. Let's see what the Send/Receive module does: private void BackgroundThreadWork()
{
foreach (StudentModel model in this._Students)
{
// Do the synchronization
try
{
// 1. Send changes
worker.ReportProgress((int)
StudentListViewItem.StatusEnum.Sending, model);
ICommand saveCommand = new SaveStudentCommand(model.Profile);
saveCommand.Execute();
// 2. Receive changes
worker.ReportProgress((int)
StudentListViewItem.StatusEnum.Receiving, model);
ICommand loadCommand = new
LoadStudentDetailCommand(model.Profile.ID);
loadCommand.Execute();
// 3. Complete
model.IsDirty = false;
model.NotifyChange("Reloaded");
model.LastErrorInSave = string.Empty;
worker.ReportProgress((int)
StudentListViewItem.StatusEnum.Completed, model);
}
catch (Exception x)
{
model.LastErrorInSave = x.Message;
worker.ReportProgress((int)
StudentListViewItem.StatusEnum.Error, model);
}
}
}
In this method, we call the web services for doing the actual work. As this method is running in a background thread, the UI does not get blocked while the web service calls are in progress. You see, the tasks are actually encapsulated inside small commands. These commands do the web service call and update the object model. I need not worry about refreshing the UI or repopulating the student list drop down from here because whenever the object model is updated, they update themselves. I need not think about any other part of the application at all and can focus on doing my duties which is relevant to this context only. So you see, whenever you add a new module to your application, you need not worry about changing existing modules' code in order to utilize the new module. The new module just subscribes to the object model and does its job whenever some events are raised. You can also tear off a module from any part of the application without worrying about its dependency on other modules. There is no dependency. Everyone only depends on the object model. Everyone is listening to the object model for its own purpose. No body cares who comes in and who goes away. This is the true power of an automation supported object model. Owner-drawn ListBoxYou have seen in the "Send Receive" module's screenshot that, a simple
After setting the drawing mode, subscribe to the private void errorsListBox_DrawItem(object sender, DrawItemEventArgs e)
{
Rectangle bounds = e.Bounds;
bounds.Inflate(-4, -2);
float left = bounds.Left;
e.Graphics.FillRectangle(Brushes.White, e.Bounds);
if (e.State == DrawItemState.Focus
|| e.State == DrawItemState.Selected)
ControlPaint.DrawFocusRectangle(e.Graphics, e.Bounds);
e.Graphics.DrawImage(this.imageList1.Images[3],
new PointF(left, bounds.Top));
left += this.imageList1.Images[3].Width;
RectangleF textBounds = new RectangleF( left, bounds.Top,
bounds.Width, bounds.Height);
e.Graphics.DrawString(this.errorsListBox.Items[e.Index] as string,
this.errorsListBox.Font, Brushes.Black, textBounds);
e.Graphics.DrawLine(Pens.LightGray, e.Bounds.Left, e.Bounds.Bottom,
e.Bounds.Right, e.Bounds.Bottom);
}
You will get everything you need to know about the drawing context from the parameter Owner-drawn TreeView
protected override void OnDrawNode(DrawTreeNodeEventArgs e)
{
// colors that depend if the row is currently selected or not,
// assign to a system brush so should not be disposed
// not selected colors
Brush brushBack = SystemBrushes.Window;
Brush brushText = SystemBrushes.WindowText;
Brush brushDim = Brushes.Gray;
if ((e.State & TreeNodeStates.Selected) != 0)
{
if ((e.State & TreeNodeStates.Focused) != 0)
{
// selected and has focus
brushBack = SystemBrushes.Highlight;
brushText = SystemBrushes.HighlightText;
brushDim = SystemBrushes.HighlightText;
}
else
{
// selected and does not have focus
brushBack = SystemBrushes.Control;
brushDim = SystemBrushes.WindowText;
}
}
RectangleF rc = new RectangleF(e.Bounds.X, e.Bounds.Y,
this.ClientRectangle.Width, e.Bounds.Height);
// background
e.Graphics.FillRectangle(brushBack, rc);
//base.OnDrawNode(e);
if (rc.Width > 0 && rc.Height > 0)
{
if (e.Node is CourseNode)
this.DrawCourseNode(e, rc, brushBack, brushText, brushDim);
else if (e.Node is SectionNode)
this.DrawSectionNode(e, rc, brushBack, brushText, brushDim);
else if (e.Node is RoutineNode)
this.DrawRoutineNode(e, rc, brushBack, brushText, brushDim);
}
}
Most of the code here you see are dealing with current node state and drawing the focus rectangle or a gray rectangle. At the end, according to the current node object, we are calling the actual render function. private void DrawCourseNode(DrawTreeNodeEventArgs e, RectangleF rc,
Brush brushBack, Brush brushText, Brush brushDim)
{
CourseNode node = e.Node as CourseNode;
if( (e.State & TreeNodeStates.Selected) == 0 )
{
this.GradientFill(e.Graphics, rc, Color.White,
Color.Gainsboro, rc.Height/2);
}
float textX = rc.Left; // +image.Width;
SizeF titleSize = this.DrawString(e.Graphics, node.Course.Title,
base.Font, brushText, textX, rc.Top);
float secondLineY = rc.Top + titleSize.Height;
if (!node.IsAllowed)
textX += this.DrawString(e.Graphics, "All Closed ",
base.Font, Brushes.DarkRed, textX, secondLineY ).Width;
int capacity = node.Capacity;
string count = "(" + capacity + ") ";
textX += this.DrawString( e.Graphics, count, base.Font,
brushText, textX, secondLineY ).Width;
// show number of sections
int sectioncount = node.Course.CourseSectionCollection.Count;
string sections = sectioncount.ToString() + " section"
+ (sectioncount>1?"s":"");
textX += this.DrawString(e.Graphics, sections,
base.Font, brushText, textX, secondLineY).Width;
}
We have the full power to draw text, icons using any style or font in this method. This gives us complete power over rendering of the Web Service Connection ConfigurationWhen you add reference to a webservice, the proxy code is generated with the web service location embedded in the code. So, during development, if you add web service reference from the local computer, you get the web service reference set to localhost. So, before going to production, you need to change all the web service proxies, so that they point to the production server. Another problem is, what if your web service locations are not static? What if you need to make the URL configurable from your own configuration file, instead of fixing it from Visual Studio? I solve it the following way:
So, basically I am following the Factory pattern where a factory decides which proxy to return and it also prepares the reference a bit before returning the reference. For example: internal class ServiceProxyFactory
{
/// <summary>
/// Contruct a student web service proxy ready to be called
/// </summary>
/// <returns></returns>
public static StudentService GetStudentService()
{
StudentService service = new StudentService();
service.Url = _Application.Settings.WebServiceUrl +
"StudentService.asmx";
SmartInstitute.Automation.SmartInstituteServices.
StudentService.CredentialSoapHeader header =
new SmartInstitute.Automation.SmartInstituteServices.
StudentService.CredentialSoapHeader();
header.Username = _Application.Settings.UserInfo.UserName;
header.PasswordHash = _Application.Settings.UserInfo.UserPassword;
header.VersionNo = _Application.Settings.VersionNo;
SetProxy(service);
service.CredentialSoapHeaderValue = header;
return service;
}
}
The following issues are handled here:
The Server - Service Oriented ArchitectureRequirementsThe word "Service" has the following definition as per Wikipedia:
Moreover a Service needs to be stateless:
Moreover, SOAs are:
The Sample SOA
There are four services available on the web project available in the source code:
Enterprise LibraryEnterprise Library is a major new release of the Microsoft Patterns & Practices application blocks. Application blocks are reusable software components designed to assist developers with common enterprise development challenges. Enterprise Library brings together new releases of the most widely used application blocks into a single integrated download. The overall goals of the Enterprise Library are the following:
Application blocks help address the common problems that developers face from one project to the next. They have been designed to encapsulate the Microsoft recommended best practices for .NET applications. They can be added into .NET applications quickly and easily. For example, the Data Access Application Block provides access to the most frequently used features of ADO.NET in simple-to-use classes, boosting developer productivity. It also addresses scenarios not directly supported by the underlying class libraries. (Different applications have different requirements and you will not find that every application block is useful in every application that you build. Before using an application block, you should have a good understanding of your application requirements and of the scenarios that the application block is designed to address.) (The above text is from Enterprise Library Documentation.) The sample web solution uses the following application blocks:
SecurityAuthentication & Authorization is handled by the Enterprise Library's Security Application Block. It's a feature rich block which contains the following:
It's a great block to use in your application. However, if you already have a custom database based authentication & authorization implementation, you will have to do the following in order to replace your home grown A&A codes to industry standard practices provided by the Enterprise Library:
Here's the code for authentication: public static string Authenticate( string userName, string password )
{
IAuthenticationProvider authenticationProvider =
AuthenticationFactory.GetAuthenticationProvider(
AUTHENTICATION_PROVIDER);
IIdentity identity;
byte [] passwordBytes =
System.Text.Encoding.Unicode.GetBytes( password );
NamePasswordCredential credentials =
new NamePasswordCredential(userName, passwordBytes);
bool authenticated =
authenticationProvider.Authenticate(credentials, out identity);
if( authenticated )
{
ISecurityCacheProvider cache =
SecurityCacheFactory.GetSecurityCacheProvider(
CACHING_STORE_PROVIDER);
// Cache the identity. The SecurityCache will generate
// a token which is then returned to us.
IToken token = cache.SaveIdentity(identity);
return token.Value;
}
else
{
return null;
}
}
Also for authorization, you can check whether the user is a member of a particular role: public static bool IsInRole(string userName, string roleName)
{
IRolesProvider rolesProvider =
RolesFactory.GetRolesProvider(ROLE_PROVIDER);
IPrincipal principal =
rolesProvider.GetRoles(new GenericIdentity(userName));
if (principal != null)
{
bool isInRole = principal.IsInRole(roleName);
return isInRole;
}
else
{
return false;
}
}
But checking just user's role membership is not sufficient. In complex applications, we need to provide task based authorization which means checking whether the user has permission to do a particular task or not. This can be done the following way: public static bool IsAuthorized( string userName, string task )
{
IRolesProvider roleProvider =
RolesFactory.GetRolesProvider(ROLE_PROVIDER);
IIdentity identity = new GenericIdentity(userName);
IPrincipal principal = roleProvider.GetRoles(identity);
IAuthorizationProvider ruleProvider =
AuthorizationFactory.GetAuthorizationProvider(RULE_PROVIDER);
// Determine if user is authorized for the rule defined
// e.g. "Print Document"
bool authorized = ruleProvider.Authorize(principal, task);
return authorized;
}
The Security Console application provided with the source code allows you to create user, create role and then assign user to role. More on this later on. However, defining a rule is a bit different. My first expectation was, a rule should be in the database so that it can be changed from code. Instead, it turned out to be static and defined in the application configuration file using the Enterprise Library Configuration application. Sending Password Over the WireIf you are not using HTTPS/SSL, or Integrated Windows Authentication, you will face the problem of sending password over the wire. You cannot send password as plain text because anyone can eavesdrop and get the password. So, you have to send the MD5/SHA hash of the password to the webservice. The simplest way to do this is to send credentials using public class CredentialSoapHeader : SoapHeader
{
/// <summary>
/// User name of the user
/// </summary>
public string Username;
/// <summary>
/// Hash of password, do not send password clear text over the wire
/// </summary>
public string PasswordHash;
/// <summary>
/// Version no of client. If it does not match with server's version no,
/// no request served
/// </summary>
public string VersionNo;
/// <summary>
/// Temporary security key generated for a particular client
/// When client
/// </summary>
public string SecurityKey;
}
There are two ways you can pass authentication information to the server:
On the client side, we have seen that we are using a factory for generating the web service proxy which prepares the web service proxy reference with the credentials: public static StudentService GetStudentService()
{
StudentService service = new StudentService();
service.Url = _Application.Settings.WebServiceUrl +
"StudentService.asmx";
SmartInstitute.Automation.SmartInstituteServices.
StudentService.CredentialSoapHeader header =
new SmartInstitute.Automation.SmartInstituteServices.
StudentService.CredentialSoapHeader();
header.Username = _Application.Settings.UserInfo.UserName;
header.PasswordHash = _Application.Settings.UserInfo.UserPassword;
header.VersionNo = _Application.Settings.VersionNo;
}
Now we do not send the password collected from the login box as plain text, instead we send the MD5 hash of the password: UserInfo user = _Application.Settings.UserInfo;
user.UserName = this._UserName;
string passwordHash = HashStringMD5(this._Password);
user.UserPassword = passwordHash;
using (SecurityService service = ServiceProxyFactory.GetSecurityService())
{
user.SecurityKey = service.AuthenticateUser();
if (null != user.SecurityKey && user.SecurityKey.Length > 0)
{
this._IsSuccess = true;
}
}
Authentication & Authorization from Web ServiceOn the server side, you go to each and every web method that should be called from the authenticated clients, and mark them to check for the credential SOAP header. [WebService(Namespace="http://oazabir.com/webservices/smartinstitute")]
public class StudentService : SecureWebService
{
[WebMethod]
[SoapHeader("Credentials")]
public Student GetStudentDetails( int userID )
{
base.VerifyCredentials();
return base.Facade.GetStudentDetail( userID );
}
[WebMethod]
[SoapHeader("Credentials")]
public StudentCollection GetAllStudents( )
{
base.VerifyCredentials();
return base.Facade.GetAllStudents();
}
[WebMethod]
[SoapHeader("Credentials")]
public bool SyncStudent( Student student )
{
base.VerifyCredentials();
return base.Facade.SyncStudent( student );
}
}
First, we specify the The base class called public class SecureWebService : WebService
{
public CredentialSoapHeader Credentials;
This public object is populated using the protected Facade Facade = new Facade();
protected string VerifyCredentials()
{
System.Threading.Thread.Sleep(2000);
// For simulating delay in real service
if (this.Credentials == null
|| this.Credentials.Username == null
|| this.Credentials.PasswordHash == null)
{
throw new SoapException("No credential supplied",
SoapException.ClientFaultCode, "Security");
}
if( this.Credentials.VersionNo != Configuration.Instance.VersionNo )
throw new SoapException("The version you are using" +
" is not compatible with the server.",
SoapException.VersionMismatchFaultCode, "Security");
return CheckCredential(this.Credentials);
}
// authenticates a user's credentials passed in a custom SOAP header
private string CheckCredential( CredentialSoapHeader header )
{
// If security key provided, authenticate using the security key
if( null != header.SecurityKey && header.SecurityKey.Length > 0 )
{
if( Facade.Login( header.SecurityKey ) )
return header.SecurityKey;
else
throw new SoapException("Security key is not valid",
SoapException.ClientFaultCode, "Security" );
}
else
{
// Authenticate using credential
string key = Facade.Login( header.Username, header.PasswordHash );
if( null == key )
throw new SoapException("Invalid credential supplied",
SoapException.ClientFaultCode, "Security" );
else
return key;
}
}
}
Enforcing Update from the ServerOne interesting trick you have noticed in the above code is that, we are passing The solution is to reject any client by force, which is not up-to-date. On the client application, store a configuration variable which contains the version number. On the server side, store the version number that you will allow in a configuration file. Every request contains the if( this.Credentials.VersionNo != Configuration.Instance.VersionNo )
throw new SoapException("The version you are using" +
" is not compatible with the server.",
SoapException.VersionMismatchFaultCode, "Security");
Configuring Security Application Block
Before getting started, make sure you have added Data Access Application Block in the configuration and you have mapped it properly to your database. This database will be used in all the places. Step 1: Create an Authentication Provider. I am using Database Provider. You can use other options like Active Directory Authentication Provider. Please see the EL documentation for details. After adding the Database Provider, select the database where authentication information is stored. I am using the same database SmartInstitute for storing the configuration. Step 2: Find the SecurityDatabase.sql file from EL's source code. This is the SQL which prepares the database. If you want to use the default "Security" database, then you can run it. But if you want to use your own database, open the file in an editor and perform the following search & replace:
Don't run the file yet! It will delete your database. Remove the IF NOT EXISTS (SELECT name FROM master.dbo.sysdatabases
WHERE name = N'SmartInstitute')
CREATE DATABASE [SmartInstitute]
COLLATE SQL_Latin1_General_CP1_CI_AS
GO
Step 3: Add a Database Roles Provider. Map it to the same database. Step 4: Add a Database Profile Provider. Map it to the same database. Step 5: Add a Rules Provider. This is a bit different. Right click on the provider name and create a new rule. Here's how the rule screen looks like:
Here you can define the static expression of roles and identities which match a rule. If a user and its roles match a rule, it is authorized for the rule. We have already seen before how to authorize against a rule. Step 6. Add a Security Cache provider. We will cache credentials in order to avoid database call for authentication every time a webservice method hits. Preparing the Security Console ApplicationSecurity Console application is provided with EL's source code. You will have to modify the dataConfiguration.config and define your own database name and then build a version. Authenticating using EL: The SurpriseHere's the code you need to use in order to authenticate using EL's IAuthenticationProvider authenticationProvider =
AuthenticationFactory.GetAuthenticationProvider(AUTHENTICATION_PROVIDER);
IIdentity identity;
byte [] passwordBytes = System.Text.Encoding.Unicode.GetBytes( password );
NamePasswordCredential credentials =
new NamePasswordCredential(userName, passwordBytes);
bool authenticated =
authenticationProvider.Authenticate(credentials, out identity);
Here's the catch: The password must be in plain text. But you don't have password in plain text because the client will be sending the MD5 hash of the password. You cannot either get the password from the database using any of the The easy solution is, you pass the MD5 hash of the password to So, we will be modifying the code of SecurityConsole a bit like this: // First hash the password input using simple MD5. Use the same
// hashing method here which client will be using before sending
// password to Authenticate Method
string passwordHash = HashStringMD5( tbxPassword1.Text );
byte [] unicodeBytes = Encoding.Unicode.GetBytes(passwordHash);
byte[] password = hashProvider.CreateHash(unicodeBytes);
if (editMode)
{
userRoleMgr.ChangeUserPassword(tbxUser.Text, password);
}
This is not extra load on the server because the first hash is done by the client and the second hash is done by the server. Data AccessData Access Application BlockEnterprise Library provides a convenient Data Access Application Block for all your database needs. It has a convenient This is a real example where you should not use any third party code directly, instead always use your own wrapper. No matter how standard they appear to be and from whatever reliable source they come from, once you get dependent on them and they change, you are doomed. Due to this problem, I have created a public class SqlHelper
{
public static int ExecuteNonQuery( IDbTransaction transaction,
string spName, params object [] parameterValues )
{
Database db = DatabaseFactory.CreateDatabase();
return db.ExecuteNonQuery( transaction, spName, parameterValues );
}
public static int ExecuteNonQuery( string connectionString,
string spName, params object [] parameterValues )
{
Database db = DatabaseFactory.CreateDatabase();
return db.ExecuteNonQuery( spName, parameterValues );
}
public static SqlDataReader ExecuteReader( string connectionString,
string spName, params object [] parameterValues )
{
Database db = DatabaseFactory.CreateDatabase();
return db.ExecuteReader( spName, parameterValues ) as SqlDataReader;
}
public static SqlDataReader ExecuteReader(IDbTransaction transaction,
string spName, params object [] parameterValues )
{
Database db = DatabaseFactory.CreateDatabase();
return db.ExecuteReader( transaction, spName,
parameterValues ) as SqlDataReader;
}
}
Once you make a class like this, you can have your existing code untouched and make them work with EL. However, if you were using public class SqlHelperParameterCache
{
private static readonly Hashtable _ParamCache = new Hashtable();
public static IDataParameterCollection GetSpParameterSet( string
connectionString, string storedProcName )
{
Database db = DatabaseFactory.CreateDatabase();
if( !_ParamCache.ContainsKey( storedProcName ) )
{
// Open the connection to the database
SqlConnection connection = db.GetConnection() as SqlConnection;
if( connection.State != ConnectionState.Open )
connection.Open();
SqlCommand cmd = new SqlCommand( storedProcName, connection );
cmd.CommandType = CommandType.StoredProcedure;
SqlCommandBuilder.DeriveParameters( cmd );
_ParamCache.Add( storedProcName, cmd.Parameters );
connection.Close();
}
return _ParamCache[ storedProcName ] as IDataParameterCollection;
}
}
ConcurrencyOffline work leads to concurrency problems. This is inevitable. So, you need to make all the tables concurrency aware and handle concurrency either from the code or from the Stored Procedure. This is how I handle concurrency:
At the Stored Procedure, this is the code: UPDATE dbo.[Assessment]
SET
[ActivityFee] = @ActivityFee,
[AdmissionFee] = @AdmissionFee,
...
WHERE
[ID] = @ID
AND [ChangeStamp] = @ChangeStamp
Here's how the code handles concurrency: reader = SqlHelper.ExecuteReader(connectionString, "prc_Assessment_Update",
entity.ID,entity.ActivityFee, ... );
if (reader.RecordsAffected > 0)
{
RefreshEntity(reader, entity);
result = reader.RecordsAffected;
}
else
{
//must always close the connection
reader.Close();
// Concurrency exception
DBConcurrencyException conflict =
new DBConcurrencyException("Concurrency exception");
conflict.ModifiedRecord = entity;
AssessmentCollection dsrecord;
//Get record from Datasource
if (transactionManager != null)
dsrecord = AssessmentRepository.Current.GetByID(
this.transactionManager, entity.ID);
else
dsrecord = AssessmentRepository.Current.GetByID(connectionString,
entity.ID);
if(dsrecord.Count > 0)
conflict.DatasourceRecord = dsrecord[0];
throw conflict;
}
CachingCaching Application Block is the only block where I haven't run into any issue during usage. But again, it seems that Cache Expiration callback feature is missing. Caching in a Server ClusterWhen you have a cluster of servers, you will run into caching problems. Consider this scenario:
The only solution to this problem is to use a centralized cache store. You will have one dedicated computer for handling all the cached objects that are costly to load from the database. But this single server might become your single point of failure and performance bottleneck under heavy load. Performance MonitoringEL has rich instrumentation features. It uses both WMI Events and Performance Counters throughout the application blocks giving you the opportunity to monitor performance of database calls, authentication & authorization success/failure, measure cache hit rates etc. You can utilize these performance counters in order to detect bottlenecks in your server application. You can use the Windows Performance Monitor to see how EL App Blocks are doing. Here's a screenshot which shows different performance counters from EL App Blocks: ![]() Code Generation using Code Smith TemplateCode Smith is a tool that can dramatically reduce your development time. Everyday we write common codes for database calls, object population, object to UI population and vice versa. All these can be automatically generated from a single mouse click using Code Smith. The entire server side code, except the Facade, is generated using the wonderful .NET data tiers generator. However, it generates code using the old Patterns & Practices Application Blocks. It was pretty difficult to modify these templates and generate EL specific codes. In the source code provided with this article, you will find the modified template which generates EL specific codes. ClickOnce DeploymentLet's take a look at my most favorite feature of .NET 2.0, the ClickOnce deployment. This is truly a revolutionary step towards deployment of Desktop/Smart Client applications. Deployment and versioning has never been this easy. I remember the day when we used to make our own hand coded custom installers and service releases during C++ days. Things did not change much even on VB era. It introduced DLL hell. Now with .NET, there's no DLL hell. But auto update still is not available from the .NET framework and you have to use custom libraries like the Updater Application Block in order to provide automatic update. But now with ClickOnce, everything ends here. This is the ultimate deployment you can ever have. While configuring ClickOnce, you need to make changes in the following section:
This dialog box defines the texts you see in the published website. After publishing your application by right clicking on the EXE project and selecting "Publish", you get a page like this:
After deploying, you can freely make changes in the application, add new features, fix bugs. When you are done, right click on the EXE project and click Publish again. All users get the updated version as soon as they launch the application. ClickOnce takes care of downloading the latest version and installing it.
And of course, for those reluctant users who never close the application and do not get the updated code, you can stop them whenever you want by implementing | ||||||||||||||||||||||||||||||||||