Navisphere: Flexible menu creation framework for ASP.NET MVC
I talked about Navisphere a menu-creation framework for ASP.NET MVC in a previous article. I finally got around to uploading the source.
View Navisphere on GitHub
I talked about Navisphere a menu-creation framework for ASP.NET MVC in a previous article. I finally got around to uploading the source.
View Navisphere on GitHub
.NET 3.5 brought with it the new System.DirectoryServices.AccountManagement namespace, which makes working with Active Directory so much easier. Let's take a look at how to find a user if you know their username (SAMAccountName):
private static UserPrincipal GetPrincipal(string name)
{
var context = new PrincipalContext(
ContextType.Domain, "yourdomain.com");
var principal = UserPrincipal.FindByIdentity(
context, IdentityType.SamAccountName, name);
return principal;
}
Wow, three entire lines of code. I don't know how we'll ever convince anyone to switch from the old way of doing things. :-) How about getting the user's groups?
PrincipalSearchResult<Principal> GetGroups(UserPrincipal principal)
{
return principal.GetAuthorizationGroups();
}
How about you just want to dump properties about that principal?
private static IDictionary<string, string> GetProperties(UserPrincipal principal)
{
var properties = new Dictionary<string, string>();
var directoryEntry = principal.GetUnderlyingObject() as DirectoryEntry;
var allProperties = directoryEntry.Properties;
foreach (var property in allProperties.PropertyNames)
{
var propertyName = property.ToString();
var value = string.Empty;
if (allProperties[propertyName] != null && allProperties[propertyName].Count > 0)
{
foreach (var val in allProperties[propertyName])
{
if (val != null)
{
value += ", " + val.ToString();
}
}
}
properties.Add(property.ToString(), value);
}
return properties;
}
If you try to run a UCMA client application in .NET 4.0, you might get this exception when you try to create the CollaborationPlatform:
System.IO.FileLoadException was unhandled
Message=Mixed mode assembly is built against version 'v2.0.50727' of the runtime and cannot be loaded in the 4.0 runtime without additional configuration information.
Source=Microsoft.Rtc.Collaboration
Add the following lines to your configuration file under the root configuration element to make the error go away:
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0"/>
</startup>
In a previous article, we learned how to get presence using UCMA. In this article, we'll learn how to start a conversation and send messages using UCMA.
The code for the entire program is located near the end of the article.
The process for starting a conversation is incredibly easy:
UserEndpoint create a ConversationInstantMessagingCallThe code:
public class ConnectionService
{
public ConversationCreateConversation()
{
return new Conversation(_endpoint);
}
}
public class ConversationService
{
private ConnectionService _connectionService;
private Conversation _currentConversation;
private InstantMessagingCall _currentIMCall;
public ConversationService(ConnectionService connectionService)
{
_connectionService = connectionService;
}
public IAsyncResult StartConversationWith(string contactSipUri)
{
_currentConversation = _connectionService.CreateConversation();
_currentIMCall = new InstantMessagingCall( _currentConversation);
return _currentIMCall.BeginEstablish(
contactSipUri,
new ToastMessage("New conversation"),
new CallEstablishOptions(),
EstablishCompleted,
null);
}
private void EstablishCompleted(IAsyncResult result)
{
_currentIMCall.EndEstablish(result);
}
}
Ending a conversation is similarly simple:
public class ConversationService
{
public IAsyncResult EndConversation()
{
return _currentConversation.BeginTerminate(TerminateCompleted, null);
}
private void TerminateCompleted(IAsyncResult result)
{
_currentConversation.EndTerminate(result);
}
}
The remaining task is to send messages:
public class ConversationService
{
public IAsyncResult SendMessage(string message)
{
return _currentIMCall.Flow.BeginSendMessage(message, SendMessageCompleted, null);
}
private void SendMessageCompleted(IAsyncResult result)
{
_currentIMCall.Flow.EndSendMessage(result);
}
}
using System;
using System.Net;
using Microsoft.Rtc.Signaling;
using Microsoft.Rtc.Collaboration;
namespace OCS_IM
{
public static class Program
{
public static void Main(string[] args)
{
var connection = new ConnectionService();
connection.Start().AsyncWaitHandle.WaitOne();
connection.InitEndpoint(
"sip:you@domain",
"ocs-server-name")
.AsyncWaitHandle.WaitOne();
var conversation = new ConversationService(connection);
conversation.StartConversationWith("sip:contact@domain")
.AsyncWaitHandle.WaitOne();
conversation.SendMessage("A message");
Console.WriteLine("Press any key to end program.");
Console.ReadLine();
conversation.EndConversation().AsyncWaitHandle.WaitOne();
connection.Stop().AsyncWaitHandle.WaitOne();
Console.ReadLine();
}
}
public class ConversationService
{
private ConnectionService _connectionService;
private Conversation _currentConversation;
private InstantMessagingCall _currentIMCall;
public ConversationService(ConnectionService connectionService)
{
_connectionService = connectionService;
}
public IAsyncResult StartConversationWith(string contactSipUri)
{
_currentConversation = _connectionService.CreateConversation();
_currentIMCall = new InstantMessagingCall(_currentConversation);
return _currentIMCall.BeginEstablish(
contactSipUri,
new ToastMessage("New conversation"),
new CallEstablishOptions(),
EstablishCompleted,
null);
}
private void EstablishCompleted(IAsyncResult result)
{
_currentIMCall.EndEstablish(result);
}
public IAsyncResult SendMessage(string message)
{
return _currentIMCall.Flow.BeginSendMessage(
message, SendMessageCompleted, null);
}
private void SendMessageCompleted(IAsyncResult result)
{
_currentIMCall.Flow.EndSendMessage(result);
}
public IAsyncResult EndConversation()
{
return _currentConversation.BeginTerminate(
TerminateCompleted, null);
}
private void TerminateCompleted(IAsyncResult result)
{
_currentConversation.EndTerminate(result);
}
}
public class ConnectionService
{
private CollaborationPlatform _platform;
private UserEndpoint _endpoint;
public IAsyncResult Start()
{
var settings = new ClientPlatformSettings("OCS_IM", SipTransportType.Tls);
_platform = new CollaborationPlatform(settings);
return _platform.BeginStartup(StartupCompleted, null);
}
private void StartupCompleted(IAsyncResult result)
{
_platform.EndStartup(result);
}
public IAsyncResult InitEndpoint(string currentUserSipUri, string ocsServerName)
{
var settings = new UserEndpointSettings(currentUserSipUri, ocsServerName);
_endpoint = new UserEndpoint(_platform, settings);
_endpoint.Credential = CredentialCache.DefaultNetworkCredentials;
return _endpoint.BeginEstablish(EstablishCompleted, null);
}
private void EstablishCompleted(IAsyncResult result)
{
_endpoint.EndEstablish(result);
}
public Conversation CreateConversation()
{
return new Conversation(_endpoint);
}
public IAsyncResult Stop()
{
return _endpoint.BeginTerminate(TerminateCompleted, null);
}
private void TerminateCompleted(IAsyncResult result)
{
_endpoint.EndTerminate(result);
_platform.BeginShutdown(ShutdownCompleted, null);
}
private void ShutdownCompleted(IAsyncResult result)
{
_platform.EndShutdown(result);
}
}
}
UCMA is a client-side API that allows you to integrate OCS functionality into your application. You can retrieve presence, publish presence, manage your subscriptions, and start conversations and conferences.
In this article, we'll take a look at retrieving presence information from UCMA by subscribing to the contact.
OCS identifies people through the SIP protocol. You can get a user's SIP address from Active Directory; each valid user will have a single, unique SIP address. SIP addresses generally are of the format:
sip:username@domain
Subscribing lets you get real-time presence updates per contact. So, if you are subscribed to John Smith, you will be notified any time his presence changes. Jane Smith’s presence, on the other hand, will become stale in your application because you’re not subscribed to her and you don’t get any notifications. In her case, you would have to ask OCS for her presence information every minute or ten minutes or whatever to get latest information.
Using UCMA starts with initializing the collaboration platform.
public class PresenceRetriever
{
private CollaborationPlatform _platform;
public IAsyncResult Start()
{
var settings = new ClientPlatformSettings("OCS_GetPresence", SipTransportType.Tls);
_platform = new CollaborationPlatform(settings);
return _platform.BeginStartup(InitCompleted, null);
}
private void InitCompleted(IAsyncResult result)
{
_platform.EndStartup(result);
}
}
The next step is to initialize your application endpoint:
public class PresenceRetriever
{
// ...
private UserEndpoint _endpoint;
public IAsyncResult InitEndpoint(string currentUserSipUri, string ocsServerPath)
{
var settings = new UserEndpointSettings(currentUserSipUri, ocsServerPath);
_endpoint = new UserEndpoint(_platform, settings);
_endpoint.Credential = CredentialCache.DefaultNetworkCredentials;
return _endpoint.BeginEstablish(EstablishCompleted, null);
}
private void EstablishCompleted(IAsyncResult result)
{
_endpoint.EndEstablish(result);
}
}
You can subscribe to one or more presences at a time:
public class PresenceRetriever
{
// ...
public void Subscribe(params string[] sipUris)
{
_endpoint.RemotePresence.PresenceSubscriptionCategories =
new string[] { "state" };
_endpoint.RemotePresence.PresenceNotificationReceived +=
PresenceNotificationReceived;
var targets = new List<RemotePresentitySubscriptionTarget>();
foreach(var sipUri in sipUris)
{
targets.Add(new RemotePresentitySubscriptionTarget(sipUri, null));
}
_endpoint.RemotePresence.BeginAddTargets(targets, AddTargetsCompleted, null);
}
private void PresenceNotificationReceived(object sender, RemotePresenceNotificationEventArgs e)
{
foreach(var notice in e.Notifications)
{
foreach(var category in notice.Categories)
{
var state = PresenceState.Create(category);
Console.WriteLine(state.Availability);
}
}
}
private void AddTargetsCompleted(IAsyncResult result)
{
_endpoint.RemotePresence.EndAddTargets(result);
}
}
If you run the application now, you should get notified every time the contacts you are subscribed to change their presences.
When your application is done with OCS, you need to teardown your endpoint and the platform. The code is simple:
public class PresenceRetriever
{
// ...
public IAsyncResult Stop()
{
return _endpoint.BeginTerminate(TerminateCompleted, null);
}
private void TerminateCompleted(IAsyncResult result)
{
_endpoint.EndTerminate(result);
_platform.BeginShutdown(ShutdownCompleted, null);
}
private void ShutdownCompleted(IAsyncResult result)
{
_platform.EndShutdown(result);
}
}
Notice how the startup and shutdown methods return IAsyncResults. The reason for this is that since the calls to OCS are performed asynchronously, you may get into situations where you try to establish the endpoint before the platform has been setup or you try to query for presence before the endpoint is established. The way to make sure that all the necessary steps are done is to use the result to figure out whether the operation has completed or force your calling method to wait until the operation has completed via AsyncWaitHandle.WaitOne method on your result.
Navigation is a common component of almost every web application. You show a bunch of links somewhere on your site to allow your user to easily get from one section of the application to another. If your application is complicated, the business rules surrounding the navigation are likely also complicated. For example, only a regular user can see this link while an administrator can see that link while everyone can see some other link, and, oh, there is this one link that should show up if the current context satisfied some predicate.
I had to deal with complicated navigation in two different applications, recently, and I absolutely hated having to write a long chain of if-else statements. So I spent some time creating a framework that allows me to easily configure an application's menus and all the business rules surrounding the display, availability, and execution of each menu item.
The framework, dubbed Navisphere, is built for ASP .NET MVC in mind, but it can pretty easily be extended to regular .NET web applications, as well.
There are three things about a menu item that might vary from user to user, all of which Navisphere handles:
And, of course, the display, execution, and availability of a menu item can also be context-sensitive (i.e., dependent on something other than roles).
The idea is to be able to write something like the following on each page load to get a list of menu items that you have to show to the user:
var menu = menuCreator.CreateFor(myContext);
Our application's menu has three items with the following business rules:
Menu Item 1
- Text = "Home" for everyone
- Is available to everyone
- Goes to admin/index for administrators and home/index for regular users
Menu Item 2
- Text = "Edit" for everyone
- Is available to administrators
- Goes to admin/edit for everyone
Menu Item 3
- Text = "Manage User" for administrator and "Manage My Profile" for user
- Is available to everyone
- Goes to user/edit for everyone
Let's setup the menu with Navisphere:
MenuCreator
.Setup(item => item
.IsShownTo.Everyone.As("Home")
.IsAvailableTo.Everyone
.IsExecutedBy.Roles("Administrator").Via("index", "admin")
.IsExecutedBy.Roles("Regular").Via("index", "home"))
.Setup(item => item
.IsShownTo.Everyone.As("Edit")
.IsAvailableTo.Roles("Administrator")
.IsExecutedBy.Everyone.Via("edit", "admin"))
.Setup(item => item
.IsShownTo.Roles("Administrator").As("Manage User")
.IsShownTo.Roles("Regular").As("Manage My Profile")
.IsAvailableTo.Everyone
.IsExecutedBy.Everyone.Via("edit", "user"));
And, to display the menu:
<ul>
<% foreach(var link in MenuBoostrapper.Menu) { %>
<li>
<%= Html.ActionLink(
link.DisplayText,
link.Action,
link.Controller,
null,
null) %>
</li>
<% } %>
</ul>
Where MenuBootstrapper is:
public static class MenuBootstrapper
{
private static readonly IMenuCreator<HttpContext> _menuCreator;
public static IMenu Menu
{
get { return _menuCreator.CreateFor(HttpContext.Current); }
}
static MenuBootstrapper()
{
// Setup the _menuCreator here.
}
}
Except in simple websites, navigation almost always ends up getting a little (or a lot) complicated. Instead of littering your code with a bunch of if or switch statements, a far better option is to create a small framework once that will allow you to setup your menus according to your business rules in a way that developers can understand and modify easily.
The framework will be available for download soon.
OpenID is a centralized membership mechanism. You register at one site and use those credentials with all other sites that you want to be a member of. This means that you have only one username and password set to memorize and, more importantly, that password is stored at only one location. When you want to log into a site that is enabled with OpenID authentication, you're forwarded to your OpenID provider. When you login, you are redirected back to the original site with your unique authorization token.
There are a lot of OpenID providers out there, including Google, Yahoo!, and Flickr. If you don't want to use one of the available providers, you can always roll your own OpenID provider.
Setting up your website for OpenID authentication is typically super-easy.
There are a couple of different OpenID frameworks for .NET, but the one with the most active development is DotNetOpenAuth. On top of providing OpenID support for both the 1.x and 2.0 versions, DotNetOpenAuth also implements OAuth, which is a protocol for secure authorization. DotNetOpenAuth supports OpenID development in ASP, classic ASP .NET web applications, as well as ASP .NET MVC websites.
In this article, we're going to talk about how to setup OpenID authentication in your ASP .NET MVC website. Our process will be as follows:
Our login view page is fairly simple: it includes a textbox for the user to enter the OpenID provider url. There are quite a few javascript libraries out there that make this a one-click process. Check out ID Selector.
<h2>Login with OpenID</h2>
<% Html.BeginForm("Authenticate", "Login"); %>
<fieldset title="Login via OpenID">
<% Html.RenderPartial("ErrorControl"); %>
<p>
<label for="openIdIdentifier">
Manually enter your OpenID URL: </label>
<%= Html.TextBox("openIdIdentifier", null, new { @class = "openId" }) %>
<input type="submit" value="Login" />
</p>
</fieldset>
<% Html.EndForm(); %>
Once the Login button is clicked, we get taken to the Authenticate action, which looks like this:
public ActionResult Authenticate(string openIdIdentifier)
{
var openId = new OpenIdRelyingParty();
var response = openId.GetResponse();
if (UserNeedsToLogin(response))
{
return AskUserToLogin(openIdIdentifier, openId);
}
return HandleAuthenticationResponse(response);
}
private bool UserNeedsToLogin(IAuthenticationResponse response)
{
return response == null;
}
private ActionResult AskUserToLogin(string openIdIdentifier, OpenIdRelyingParty openId)
{
var request = openId.CreateRequest(openIdIdentifier);
return request.RedirectingResponse.AsActionResult();
}
private ActionResult HandleAuthenticationResponse(IAuthenticationResponse response)
{
switch(response.Status)
{
case AuthenticationStatus.Authenticated:
return HandleSuccessfulLogin(response);
case AuthenticationStatus.Canceled:
_context.ErrorMessage = "Login was cancelled at the provider.";
break;
case AuthenticationStatus.Failed:
_context.ErrorMessage = "Login failed at the provider.";
break;
case AuthenticationStatus.SetupRequired:
_context.ErrorMessage = "The provider requires setup.";
break;
default:
_context.ErrorMessage = "Login failed.";
break;
}
return View("Index");
}
The code for successful login looks as expected given our requirements:
private ActionResult HandleSuccessfulLogin(IAuthenticationResponse response)
{
var claimedIdentifier = response.ClaimedIdentifier;
registerIfUserIsNotRegisteredYet(claimedIdentifier);
currentUserIs(claimedIdentifier);
if (_context.CurrentUser.HasYetToLogin)
{
return RedirectToAction("FirstTimeLogin");
}
currentUserIsLoggedIn();
return View();
}
private void currentUserIsLoggedIn()
{
_accountRepository.Login(_context.CurrentUser);
}
If it's the first time the user is logging in, we register the user with the identifier returned by the OpenID provider.
private void registerIfUserIsNotRegisteredYet(Identifier claimedIdentifier)
{
if (_accountRepository.Fetch(claimedIdentifier) == null)
{
var regularRole = new string[] { "Regular" };
var userToRegister = new Account(claimedIdentifier, regularRole)
{
Id = 0,
LastLoginAt = DateTime.MinValue,
Profile = new Profile { DisplayName = claimedIdentifier }
};
_accountRepository.Insert(userToRegister);
}
}
Account implements IPrincipal, and Profile has only one property for now (DisplayName). (You can add any other properties that make sense to your application, like FirstName, LastName, Email, etc.)
We also need to let our application know who the current user is. I do that by setting a "CurrentUser" property on my "context" object:
private void currentUserIs(Identifier claimedIdentifier)
{
var registeredUser = _accountRepository.Fetch(claimedIdentifier);
_contextUser.CurrentUser = registeredUser;
}
Normally, I use the username for both Identity.Name and DisplayName. But with OpenID, the identifiers can be long, bizarre, and useless, so we need the user to provide us a display name. When the user provides us with the display name, we update our repository with the information. Since we figure out whether or not the user has logged in before based on the last login date, we don't set it for first-time users until after the user has provided all the required information and we've updated our repository with the information. For all other users, we set the last login date as soon as the user has logged in.
[HttpGet]
public ActionResult FirstTimeLogin()
{
return View();
}
[HttpPost]
public ActionResult FirstTimeLogin(string displayName)
{
if (string.IsNullOrWhiteSpace(displayName))
{
ModelState.AddModelError("displayName", "Display name is required.");
return View();
}
var userName = _context.CurrentUser.Identity.Name;
_accountRepository.UpdateDisplayName(userName, displayName);
currentUserIsLoggedIn();
return RedirectToAction("Index", "Home");
}
Integrating OpenID with your ASP.NET MVC website feels more complicated than it really is. While we had to write a lot of code, none of it is particularly complicated or unintuitive. DotNetOpenAuth and ASP.NET MVC make it especially easy.
A common requirement for web applications is validation. Almost every action follows the same kind of pattern: if the data is invalid, inform the user of the errors and show them the form again. Otherwise, process the request and take them to another page. If you're using ASP .NET MVC, you will notice this kind of code all over the place:
public ActionResult SomeView()
{
if (!ModelState.IsValid)
{
return View();
}
// Do the real processing here, and
// redirect to some other view when
// you're done.
}
We all know that duplication is the number one enemy of maintainable code. If there is some way to not have to type in the "ModelState.IsValid" part for 50+% of the methods, then we save ourselves a lot of coding and maintaining. If you think about it, while the validation itself is different from method to method, how to respond to the validation is not. Ideally, we would like to write something like this:
public ActionResult SomeView()
{
// Do the real processing here, and
// redirect to some other view when
// you're done.
}
Fortunately, ASP .NET MVC makes it pretty easy to do this.
The first step is to figure out how to validate each of the parameters passed into the action. Since validation will be different for each parameter, this means that we have to call some method on some class to perform the actual validation.
public interface IValidator<T>
{
void Validate(T item);
}
However, we also need a way to tell the controller when some field on the model has an error on it so that it can tell the user. The user might also want to show when the value of some field is valid (maybe draw a green border around the field):
public interface IValidator<T>
{
void Validate(T item,
Action<IValidationError> notifyOnError,
Action<IValidationSuccess> notifyOnSuccess);
}
IValidationError and IValidationSuccess are what you would expect:
public interface IValidationResult
{
string PropertyName { get; }
T PropertyValue<T>();
}
public interface IValidationSuccess : IValidationResult
{
}
public interface IValidationError : IValidationResult
{
string ErrorMessage { get; }
}
The next step is to figure out whether or not a particular parameter supports validation. If so, then we need a way to get the validator for that parameter. This is pretty simple: we create an interface that all models which support validation will have to implement:
public interface IValidatable<T>
{
T GetValidator();
}
The next challenge is to figure out where to send the user if there is an error. Since where to send the user might be different for different actions, our controllers will implement a method that will give us the error view given a context:
public interface IValidatableController
{
ViewResult ErrorViewGiven(ActionExecutingContext context);
}
The idea is that every time an appropriate action happens, we iterate through all the parameters that are sent. If the parameters support validation, then we get the Validator for the parameter and call the Validate method on it. Once we're done with validating all Validatable parameters, we check to see if there were any errors. If there were errors, we perform the error operation. Otherwise, we continue to the actual method. ASP .NET MVC provides a way for us to hook into the execution of a controller action via the ActionFilterAttribute. The method we are interested in is the OnActionExecuting, which is called -- well -- just before the action is executed.
IValidatableController. If not, we have nothing to do.ModelState.AddModelErrorIValidatableValidator for each of these parameters to perform the validation.The code:
public class ValidateAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!(filterContext.Controller is IValidatableController))
{
return;
}
var validatableController = filterContext.Controller as IValidatableController;
Action<IValidationError> errorHandler = x =>
filterContext.Controller.ViewData.ModelState
.AddModelError(x.PropertyName, x.ErrorMessage);
foreach(var parameter in filterContext.ActionParameters)
{
if (!shouldValidateParameter(parameter.Value))
{
continue;
}
dynamic validatable = parameter.Value;
dynamic validator = validatable.GetValidator();
validator.Validate(validatable, errorHandler, null);
if (!filterContext.Controller.ViewData.ModelState.IsValid)
{
filterContext.Result = validatableController.ErrorViewGiven(filterContext);
}
}
}
private bool shouldValidateParameter(object parameter)
{
return parameter.GetType().GetInterfaces().Any(x =>
x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IValidatable<>));
}
}
Note the use of "dynamic" to get the validator. We need to declare dynamic variables because C# doesn't allow us to cast parameters to their generic types without knowing the actual types. Fortunately, per C# 4.0, we are able to declare variables as dynamic, which causes the code to be late-bound.