|
|
July 27th, 2010
I am on a quest to deliver SSRS reports within a web application. So far we have authenticated against Active Directory using Forms Auth. Now we need to embed a report in our app. Then, we’ll need to forward the logged-on user’s AD credentials to the report server so he can access the report.
For now, I’m just going to add a report to Default.aspx. Later on, we’ll provide a menu to navigate the user’s reports. First, drag a “MicrosoftreportViewer” control from the Toolbox onto the designer. Then, click the task button, open “Choose Report”, and select “<Server Report>”. Fill in your Report Server URL, which you can get by launching “Reporting Services Configuration Manager” and selecting the Web Service URL. Enter the path to the report that you created earlier. Your page source should look like this.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication1._Default" %>
<%@ Register assembly="Microsoft.ReportViewer.WebForms, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" namespace="Microsoft.Reporting.WebForms" tagprefix="rsweb" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<rsweb:ReportViewer ID="ReportViewer1" runat="server" Font-Names="Verdana"
Font-Size="8pt" Height="400px" Width="100%" ProcessingMode="Remote">
<ServerReport ReportPath="/Admin Report"
ReportServerUrl="http://dit3074lt2:8080/ReportServer_SQL2008R2" />
</rsweb:ReportViewer>
</div>
</form>
</body>
</html>
If you run the application right now, it looks like it all works! That’s because you are running a local web server under your own account. Your credentials are passed to the report server. We want to pass the logged-in user’s credentials to Report Server, so he can only access the reports to which he has been given permission.
Provide credentials to report Server
To pass credentials to Report Server, we set the ReportViewer.ServerReport.ReportServerCredentials property to an IReportServerCredentials. There are many ways to implement this interface. The simplest way is to provide a NetworkCredentials object.
public class NetworkReportServerCredentials : IReportServerCredentials
{
private string _userName;
private string _password;
private string _domain;
public NetworkReportServerCredentials(string userName, string password, string domain)
{
_userName = userName;
_password = password;
_domain = domain;
}
public bool GetFormsCredentials(out Cookie authCookie, out string userName, out string password, out string authority)
{
authCookie = null;
userName = null;
password = null;
authority = null;
return false;
}
public WindowsIdentity ImpersonationUser
{
get { return null; }
}
public ICredentials NetworkCredentials
{
get { return new NetworkCredential(_userName, _password, _domain); }
}
}
This implementation only provides a meaningful implementation to the NetworkCredentials property. It just returns the credentials that it was given. We have to get the user’s credentials into this object when we access the report. We captured the user’s credentials when he logged in, but we didn’t save them anywhere. We need to save them during login so we can access them while viewing the report.
During login, we have the opportunity to populate a FormsAuthenticationTicket. This is the object that gets stored in the user’s Forms Authorization cookie. This class holds the user’s name, but not their password. However, it does have a UserData property that we can use however we want.
Cryptography required
Now, we could just put the plaintext password into UserData. But we aren’t going to do that. This object is cached as a cookie in the user’s browser. Storing their plaintext password in a cookie would reveal it to anyone who had access to their machine. Even without direct access to the machine, a cross-site scripting attack could compel the browser to give up its cookie. We will not expose the user to that kind of vulnerability.
Instead, we are going to encrypt the password using a secret key that we store on the server. An attacker would need access to this key if they were going to pull the user’s password from their cookie. First, we create some useful crypto helpers:
public class Crypto
{
public static byte[] EncryptMessage(
string messageIn,
SymmetricAlgorithm symmetricAlgorithm,
byte[] key,
byte[] initializationVector)
{
MemoryStream memoryStream = new MemoryStream();
using (StreamWriter cryptoWriter = new StreamWriter(
new CryptoStream(
memoryStream,
symmetricAlgorithm.CreateEncryptor(key, initializationVector),
CryptoStreamMode.Write)))
{
cryptoWriter.Write(messageIn);
}
return memoryStream.ToArray();
}
public static string DecryptMessage(
byte[] encryptedMessage,
SymmetricAlgorithm symmetricAlgorithm,
byte[] key,
byte[] initializationVector)
{
MemoryStream memoryStream = new MemoryStream(encryptedMessage);
using (StreamReader cryptoReader = new StreamReader(
new CryptoStream(
memoryStream,
symmetricAlgorithm.CreateDecryptor(key, initializationVector),
CryptoStreamMode.Read)))
{
return cryptoReader.ReadToEnd();
}
}
public static byte[] ComputeStringHash(string message, HashAlgorithm hashAlgorith)
{
byte[] messageBytes = ASCIIEncoding.ASCII.GetBytes(message);
return hashAlgorith.ComputeHash(messageBytes);
}
}
The first two methods encrypt and decrypt a string using a symmetrical algorithm. The encrypted message is binary, so it is represented as a byte array. I found it exceedingly difficult to get these steps right, even though the code turned out to be almost trivial. The third method hardly deserves to be included with the others, but it comes in handy.
Let’s see how these methods are used.
[TestClass]
public class CryptoStreamTest
{
private const string PreGeneratedKey = @"s03UsP/dHD0=";
private SymmetricAlgorithm _symmetricAlgorithm = new DESCryptoServiceProvider();
private HashAlgorithm _hashAlgorith = new MD5CryptoServiceProvider();
private RandomNumberGenerator _randomNumberGenerator = new RNGCryptoServiceProvider();
private static byte[] _key = Convert.FromBase64String(PreGeneratedKey);
public TestContext TestContext { get; set; }
[TestMethod]
public void GenerateKey()
{
byte[] key = new byte[_symmetricAlgorithm.KeySize / 8];
_randomNumberGenerator.GetBytes(key);
string encodedKey = Convert.ToBase64String(key);
Assert.AreNotEqual(PreGeneratedKey, encodedKey);
Console.WriteLine(encodedKey);
}
[TestMethod]
public void EncryptAndDecryptStream()
{
byte[] initializationVector = new byte[_symmetricAlgorithm.KeySize / 8];
_randomNumberGenerator.GetBytes(initializationVector);
byte[] encryptedMessage = Crypto.EncryptMessage("plaintext", _symmetricAlgorithm, _key, initializationVector);
string messageOut = Crypto.DecryptMessage(encryptedMessage, _symmetricAlgorithm, _key, initializationVector);
Assert.AreEqual("plaintext", messageOut);
}
[TestMethod]
public void InitializationVectorIsImportant()
{
byte[] initializationVector1 = new byte[_symmetricAlgorithm.KeySize / 8];
_randomNumberGenerator.GetBytes(initializationVector1);
byte[] initializationVector2 = new byte[_symmetricAlgorithm.KeySize / 8];
_randomNumberGenerator.GetBytes(initializationVector2);
byte[] encryptedMessage = Crypto.EncryptMessage("plaintext", _symmetricAlgorithm, _key, initializationVector1);
string messageOut = Crypto.DecryptMessage(encryptedMessage, _symmetricAlgorithm, _key, initializationVector2);
Assert.AreNotEqual("plaintext", messageOut);
}
[TestMethod]
public void HashMessage()
{
byte[] hash = Crypto.ComputeStringHash("This is the string that we intend to sign.", _hashAlgorith);
string encodedHash = Convert.ToBase64String(hash);
Assert.AreEqual(@"04fj0UWULE9imGTrHRUw5g==", encodedHash);
}
}
To generate a key, we use the cryptographic random number generator RNGCryptoServiceProvider. This produces a binary array of the key length required by our symmetrical encryption algorithm. Convert the binary array to a base 64 string for easy portability. I generated one ahead of time for use with the remaining tests.
To encrypt and decrypt a string, we must provide not only a key but also an initialization vector. The initialization vector is a starting point for the symmetrical algorithm. Encryption and decryption must both start at the same point. The trick is that we don’t want to use the same initialization vector every time, because that would mean we always produce the same cyphertext for a given plaintext. An attacker could simply create an account with a common password (say, “password”), and then look for other users with the same cyphertext as he has. Bingo! He knows that their password is “password”!
Finally, we test computing the hash of a string. This is usually used to digitally sign a message, but we have another use for it.
Encrypt the user’s password
We want to encrypt the user’s password. For that we’ll need a key; you can generate one with the first unit test above. But we will also need an initialization vector. Remember, we need to use the same initialization vector for encrypting as well as decrypting. And we’ll also need it to be different for each user. The simple solution: use the hash of the username.
protected void Login1_Authenticate(object sender, AuthenticateEventArgs e)
{
string username = Login1.UserName;
string password = Login1.Password;
// MLP: Encrypt the password.
byte[] usernameHash = Crypto.ComputeStringHash(username, _hashAlgorith);
byte[] encryptedMessage = Crypto.EncryptMessage(password, _symmetricAlgorithm, _key, usernameHash);
string encryptedPassword = Convert.ToBase64String(encryptedMessage);
if (Membership.ValidateUser(username, password))
{
e.Authenticated = true;
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1,
username,
DateTime.Now,
DateTime.Now.AddMinutes(30),
false,
encryptedPassword,
FormsAuthentication.FormsCookiePath);
// MLP: This method should be called "Encode", not "Encrypt".
string encTicket = FormsAuthentication.Encrypt(ticket);
// Create the cookie.
Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));
// Redirect back to original URL.
Response.Redirect(FormsAuthentication.GetRedirectUrl(username, false));
}
else
{
e.Authenticated = false;
}
}
Hold on a second. If we are calling FormsAuthentication.Encrypt(), why are we bothering to encrypt the password first? Unfortunately, Encrypt() is not secure. It doesn’t take a key. That means that anyone with access to the .NET Framework can simply call Decrypt() to get back the ticket. I would prefer if the method was called Encode().
Decrypt the user’s password
The last step is to access the encrypted password, decrypt it, and pass the credentials to Report Server. We accomplish this with a minimum of code:
protected void Page_Load(object sender, EventArgs e)
{
// MLP: Get the user's credentials from forms auth.
IIdentity identity = HttpContext.Current.User.Identity;
FormsIdentity formsIdentity = (FormsIdentity)identity;
string username = formsIdentity.Name;
string encryptedPassword = formsIdentity.Ticket.UserData;
// MLP: Decrypt the password.
byte[] usernameHash = Crypto.ComputeStringHash(username, _hashAlgorith);
byte[] encryptedMessage = Convert.FromBase64String(encryptedPassword);
string password = Crypto.DecryptMessage(encryptedMessage, _symmetricAlgorithm, _key, usernameHash);
IReportServerCredentials credentials = new NetworkReportServerCredentials(username, password, "ABSG");
ReportViewer1.ServerReport.ReportServerCredentials = credentials;
}
And so we have passed the user’s network credentials on to Report Server. They were authenticated against Active Directory, which is the identity provider that SSRS prefers. However, we did all this in forms authentication so that our web application works better with users on the Internet.
Next steps
Next we are going to provide a menu of reports to the user. The business administrator should be able to define new reports without requiring any change to our application.
Posted in Reports | No Comments »
July 26th, 2010
We have reports published to SQL Server Reporting Services, and we want users to access those reports from a web application. We could give them direct access to Report Manager, but we choose not to. The Report Manager UI displays concepts in the reporting domain, not concepts in our problem domain (healthcare). We want to give the users a simpler, branded experience, while still giving them access to reports created by a business administrator, not a developer.
SSRS uses Active Directory for authentication. While it is theoretically possible to change the authentication provider, this is exceedingly difficult. In prior releases, SSRS ran both the Report Manager and the Web Service in IIS, which lets you to choose to allow anonymous access. As of SQL Server 2008, SSRS hosts these services itself. It does not expose the anonymous access option. So even if you change the authentication provider for the reports, you must first get past the web server security. The net effect is that your users need to be in Active Directory.
Fortunately for us, we use Active Directory for authentication. We just don’t use Windows Authentication for the web application. That makes this design feasible.
Use Forms Authentication When you create a new ASP .NET web application, it is initially configured to use Windows Authentication. This allows a user within the domain to use their credentials to access the application without re-authenticating. Since they are logged in to Windows, those credentials get passed through to the app.
In our case, however, our users are not logged in to our domain. They access the application over the Internet. They may not even be running Windows. So we have to use Forms Authentication.
Even though our users aren’t on our domain, we still use Active Directory as an identity store. Fortunately, it’s possible to use Forms Authentication with Active Directory.
Create a new ASP.NET Web Application project. This can also be done with MVC, but our existing application was written prior to its release. Add a new Web Form to the project called “Login.aspx”. Add an asp:Login control to the page. You can just drag one from the toolbox.
Double-click the login control to handle the Authenticate event. This event will be called when the user presses the Login button. For now, we’ll use FormsAuthentication.Authenticate. We’ll change that in a little bit.
<body>
<form id="form1" runat="server">
<div>
<asp:Login ID="Login1" runat="server" onauthenticate="Login1_Authenticate">
</asp:Login>
</div>
</form>
</body>
protected void Login1_Authenticate(object sender, AuthenticateEventArgs e)
{
e.Authenticated = FormsAuthentication.Authenticate(Login1.UserName, Login1.Password);
}
Edit the web.config file to use forms authentication. Create a user credential to make sure everything is working so far.
<!-- MLP: Changed this from "Windows" to "Forms". -->
<authentication mode="Forms">
<!-- MLP: Added Login.aspx and .ASPXFORMSAUTH settings. -->
<forms loginUrl="Login.aspx" name=".ASPXFORMSAUTH">
<credentials passwordFormat="Clear">
<user name="test" password="pass"/>
</credentials>
</forms>
</authentication>
<!-- MLP: Deny unauthenticated users access to other pages. -->
<authorization>
<deny users="?" />
</authorization>
Hit F5 and test your site. You should be redirected from your default page to the login page. If you enter the wrong credentials, you’ll get an error. If you enter user “test” and password “pass”, you’ll get to your default page.
Set up the Active Directory membership provider
Now that we have forms authentication working, let’s switch to using membership. “Membership” is a provider-based system for managing identity and role-based security. By default, membership uses a SQL database to store credentials. You can switch to the Active Directory provider instead.
Active Directory is very similar to a relational database. You access it via a connection string. Access is restricted to specific users. The main difference is that AD is a hierarchical store, while a database is a relational store. AD is typically used to store information about users, groups, and machines within a domain. That’s what the Active Directory membership provider expects.
If you didn’t set up AD yourself, you will need to talk to the person who did. Get a connection string, username, and password that gives you read-only access to the server. If you would like to try this yourself before involving your network operator, you can set up a virtual network in Microsoft Virtual PC.
To configure the Active Directory membership provider, add this to web.config after the <authorization> tag you added earlier. Pay close attention to the enablePasswordReset and attributeMapUsername settings. These are not mentioned in the Patterns and Practices guidance, but I found them to be necessary while working in my environment. The AD account that I have does not have permission to reset passwords. And my company’s directory does not set the userPrincipleName, which is the default.
<!-- MLP: Use the Active Directory membership provider. -->
<membership defaultProvider="ADMembershipProvider">
<providers>
<add
name="ADMembershipProvider"
type="System.Web.Security.ActiveDirectoryMembershipProvider, System.Web, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
connectionStringName="ADConnectionString"
connectionUsername="MyDomain\MyADUserName"
connectionPassword="MyADPassword"
enablePasswordReset="false"
attributeMapUsername="sAMAccountName"/>
</providers>
</membership>
Replace MyDomain and MyADUserName with the correct values. Remove the <credentials> element from <forms>. You won’t need the hard-coded credentials any more. Add the connection string to the top of the file:
<connectionStrings>
<!-- MLP: Added connection string for Active Directory authentication. –>
<add name="ADConnectionString" connectionString="LDAP://MyADMachine/DC=MyDomain,DC=MyTLD" />
</connectionStrings>
Again, replace MyADMachine, MyDomain, and MyTLD with the values you get from your network admin. You might already have a <connectionStrings> section at the top. Just make sure you have a closing tag and insert the <add …> line.
Finally, we need to change the code to use Membership instead of FormsAuthentication. Change the code that you added before to Login1_Authenticate.
protected void Login1_Authenticate(object sender, AuthenticateEventArgs e)
{
e.Authenticated = Membership.ValidateUser(Login1.UserName, Login1.Password);
}
Now run the program again and try to log in with your network credentials. In fact, you can delete your onauthenticate handler altogether. The line of code above is exactly what the login control does by default.
Next steps
Now that we are logged in using an Active Directory account, we’ll access an SSMS report using those credentials.
Posted in Reports | No Comments »
July 23rd, 2010
I’ve been researching SQL Server Reporting Services. Our goal is to let users of our application view reports created by a business administrator. The application uses Active Directory via forms authentication. It has its own custom authorization.
There are two ways to create a report in SSRS. One way is to use Visual Studio. The second is to use the Report Builder on line in the Report Manger site. I’ll use Visual Studio to get started. I’m using SQL Server 2008 R2 and Visual Studio 2008.
Create a Report Server Project
First create a Report Server project. I’m using the Report Server Project Wizard in the Business Intelligence Projects section. If you don’t see Business Intelligence Projects, please be sure to install SQL Server Reporting Services on your development box.
Create a new data source by setting up a connection string. Since we are creating application reports, we connect to the development application database. You can hit the “Edit” button to help you out, or just copy the connection string from the web.config file of your application.
My application uses a specific SQL account to access the database. This is not the end-user’s account. I entered these credentials while creating the connection string. The username and password are not visible in the generated connection string, but I can see that they’ve been stored if I hit the “Credentials” button.
Next the wizard prompts you to create a query. Define a query that selects and groups the data the way that it will be displayed on the report. The report can do little more than place the data in the right boxes. You’ll need the query to do the heavy lifting.
Then, you create your report. I selected a “Matrix” report, which allows me put data on both rows and columns. You might also know this as a “crosstab” report. My query has four columns, which is the ideal number for a matrix report. I added the highest-level concept to the “Page” box, the grouping concepts to “Rows” and “Columns”, and the lowest-level value to “Details”. Avoid putting more than one column in these boxes, because that will turn a simple matrix report into a complex tree.
Finally, give the URL of your reporting server. You can find this URL by running “Microsoft SQL Server 2008 R2”, “Configuration Tools”, “Reporting Services Configuration Manager”. Click on “Web Service URL” and select the hyperlink.
Deploy the report You now have a project containing a single .rdl file. This is a report definition that SSRS can run. You can run the report now by pressing F5. For others to run the report, it will need to reside on the report server.
By default, the project is not deployable. Select “Build”, “Configuration Manager” to change this. Check the box under “Deploy” for your report project.
To deploy the project, select “Build”, “Deploy Solution”. The .rdl file will be copied to the reporting server. You can now see the report on line. Go back to the Reporting Services Configuration Manager, but this time choose the “Report Manager URL” section. Click the hyperlink to open the report manager in the browser.
The report was deployed to a folder that you named in the wizard. Mine is in IONReports. Click on the folder, and then click on the report to view it.
You probably want other people to be able to access the report. Right now, it is probably deployed to SSRS running on localhost. To deploy it to a remote server, first ensure that SSRS is installed on the target machine. Then you can run the Reporting Services Configuration Manager on that server to find the Web Service URL.
Right click on the report server project in Visual Studio and choose “Properties”. This brings up a property page where you can edit the settings that you configured using the wizard. Enter the remote server’s Web Service URL in the TargetServerURL. You can now deploy the report to a server where others can run it.
Once the report is deployed, others can edit it on line using the Report Manager. Instead of clicking to run the report, pull down the menu. Choose “Edit in Report Builder”. The report builder is not quite as slick as the Visual Studio wizard, but it gets the job done. The connection, query, and report layout that you created in Visual Studio are all editable from within the report builder. You just might have to use text instead of a visual interface.
A business admin can edit and create reports using this interface. It may be easier for everyone if a developer or DBA designs the queries to start with. A developer would most likely be using a Visual Studio report server project, whereas the DBA would probably create a view. But once they have help getting started, the business admin can take over.
Next steps Remember, the goal is to access reports from within an application. The admin uses the Report Manager, but the end user needs a simpler, branded interface. We’ll tackle that in the next article.
Posted in Reports | No Comments »
July 16th, 2010
If you use Update Controls, please get the latest release. In addition to changing from LGPL to the MIT license, this one supports Visual Studio 2010 and Silverlight 4 better. It also includes a new ViewModelBase class for better control of your view models.
MIT License My goal is to improve software quality. That is best accomplished by giving people the patterns and tools to develop the best software they can. That is why Update Controls is open source. If you can see how it works, you don’t have to fear it. The MIT license supports wide adoption by placing no additional burden on the user of the library.
Visual Studio 2010 While I’ve ported the project to VS2010, the library can still be used in VS2008. It still targets .NET 3.5, and will continue to do so for the foreseeable future. But Visual Studio has changed the way that it populates the Add Reference dialog. This new installer works better with that mechanism.
Silverlight 4 In addition to targeting Silverlight 3, the project also targets Silverlight 4. While Silverlight 4 can load assemblies built for 3, I found that that configuration was not as stable as a targeted Silverlight 4 build. Both assemblies are installed. Visual Studio chooses the correct assembly in the Add Reference dialog based on your project type.
ViewModelBase Sometimes the wrapper gets in the way. If you are using paged collection views, data grids, or RIA services, you want the view to see the attributes on your view model. ForView.Wrap() obscures these attributes. In those situations, you can use ViewModelBase instead. Just call Get(() => ...) or GetCollection(() => ...) inside your property getters. You can put whatever logic you need to inside the lambda expression.
When Update Controls was originally created, I favored composition over inheritance. Since then, the MVVM pattern has become quite popular. Many of these new frameworks have a class called ViewModelBase that implements INotifyPropertyChanged. Since the community appears to accept giving up their base class for this purpose, I decided that Update Controls should offer that as an option. But ForView.Wrap() and the Update markup extension are still fully supported. The choice is yours.
Posted in Update Controls | No Comments »
July 12th, 2010
I’m a practitioner of Merlin Mann’s Inbox Zero. The basic idea is that you avoid building up psychic weight by touching email as few times as possible. For each message you must:
- Do it
- Defer it
- Delegate it
- Delete it
I have created an Outlook toolbar with these actions. Delete comes first because it is by far the most frequent response. Defer comes next, but is rarely used. Delegate is last to remind me not to become part of the problem. There are only three buttons because after I do it, I delete it.
Part of the freedom of inbox zero comes from allowing yourself to delete messages. You can only truly do this if you know that the information is not irrevocably lost. For that reason, my delete button is not the standard Outlook delete. It just moves the message to an “Archive” folder. Not a categorized, taxonomized, organized folder. Just one big archive that I can search if necessary. I have to search it about twice a month, and I’ve never had trouble finding what I needed.
I’ve written Outlook macros for two of these buttons. They just mark the current message as read and move them to the correct folder: “Archive” for delete, and “Open” for defer.
Function GetFolder(ByVal folderPath As String) As Outlook.Folder
Dim TestFolder As Outlook.Folder
Dim FoldersArray As Variant
Dim i As Integer
On Error GoTo GetFolder_Error
If Left(folderPath, 2) = "\\" Then
folderPath = Right(folderPath, Len(folderPath) - 2)
End If
'Convert folderpath to array
FoldersArray = Split(folderPath, "\")
Set TestFolder = Application.Session.Folders.Item(FoldersArray(0))
If Not TestFolder Is Nothing Then
For i = 1 To UBound(FoldersArray, 1)
Dim SubFolders As Outlook.Folders
Set SubFolders = TestFolder.Folders
Set TestFolder = SubFolders.Item(FoldersArray(i))
If TestFolder Is Nothing Then
Set GetFolder = Nothing
End If
Next
End If
'Return the TestFolder
Set GetFolder = TestFolder
Exit Function
GetFolder_Error:
Set GetFolder = Nothing
Exit Function
End Function
Sub Archive()
MarkReadAndMove ("\\Mailbox - Perry, Michael\Inbox\Archive")
End Sub
Sub Defer()
MarkReadAndMove ("\\Mailbox - Perry, Michael\Inbox\Open")
End Sub
Sub MarkReadAndMove(ByVal folderPath As String)
Dim selectedItem As Object
Dim archiveFolder As Outlook.Folder
Set selectedItem = Application.ActiveExplorer.Selection.Item(1)
Set archiveFolder = GetFolder(folderPath)
selectedItem.UnRead = False
selectedItem.Move archiveFolder
End Sub
Change the names as appropriate. I hope you find this as useful as I do.
Posted in Tools | No Comments »
July 4th, 2010
I’ll be speaking at NDDNUG on Wednesday about the CAP Theorem and its implications. Here’s a preview of what you can expect:
I’ll be posting slides and source code at qedcode.com. Hope to see you there.
Posted in qed | No Comments »
June 23rd, 2010
Add this class to your C# project:
public class Directory
{
private string _path;
private Directory(string path)
{
_path = path;
}
public static Directory ApplicationData
{
get
{
return new Directory(Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData));
}
}
public static Directory CommonApplicationData
{
get
{
return new Directory(Environment.GetFolderPath(
Environment.SpecialFolder.CommonApplicationData));
}
}
public static Directory operator /(Directory root, string folder)
{
return new Directory(Path.Combine(root._path, folder));
}
public static implicit operator string(Directory directory)
{
return directory._path;
}
}
Now you can express directories naturally:
string databaseFileName = Directory.ApplicationData / "MichaelLPerry" / "CorrespondenceIM" / "Correspondence.sdf";
I don’t have a good place for code like this to live. What do you do with these kinds of utilities?
Posted in C# | No Comments »
June 18th, 2010
I think we can officially call this a bug in Silverlight. Create a master list and a detail view. Bind the SelectedItem of a combo box in the detail view to a reference typed property. Silverlight will set the property to null when you select a new item in the master list.
Try it out below. The list on the left displays the person’s name and preferred phone number. Select “Michael”, and the detail pane shows his phone number and favorite color. Then select “Jenny”. Notice that Michael’s phone number disappeared. Click back on him and see that the phone number is indeed gone.
Favorite color is an enumeration – a value type. Phone number is a class – a reference type. Even though the phone number has a valid Equals and GetHashCode implementation, Silverlight treats it differently. For some reason, it decides it needs to set it to null whenever the DataContext changes.
This behavior is clearly incorrect. The list of items does not contain the value “null”. Even if it did, the user did not select “null”. And as further evidence, WPF does not behave this way. Download the source code, which has been compiled for both Silverlight and WPF. The WPF version of exactly the same source code behaves correctly.
Here’s my solution
Any property bound to SelectedItem should protect itself from a null assignment. In this case:
public PhoneNumber PreferredPhone
{
get { return _preferredPhone; }
set
{
if (value == null)
return;
_preferredPhone = value;
RaisePropertyChanged("PreferredPhone");
RaisePropertyChanged("Display");
}
}
What if null is a valid value for the property? Then represent null with a stand-in. Perhaps two classes could implement a common IPhoneNumber interface: PhoneNumber and NullPhoneNumber.
It’s a pretty simple workaround, but still an embarrassing bug in Silverlight.
Posted in Silverlight | No Comments »
May 25th, 2010
I’m working on a graphical editor in WPF. One feature of this editor is the ability to resize an object. As cool as that feature is, the comment I got from my partner on the project was even better.
In this editor, you can drag the mouse across the surface to pan. You can use your scroll wheel to zoom in and out. And you can select an object and use your scroll wheel to resize it. The trick is figuring out just how much to resize with every tick.
When the scale is zoomed out, you want each tick of the mouse to adjust the size by large amount. When the scale is zoomed in tight, you want to adjust by a proportionally smaller amount. This gives the user a chance to fine-tune by zooming in.
But on the other end of the spectrum, you have to prevent the size of the object from reaching zero. So when the object gets small, it no longer shrinks in proportion to the zoom.
Design an equation The first problem is to change the size in proportion to the zoom. The easiest way to do this is to convert the object size into screen size, adjust by a constant amount, and then convert back. Here’s the math to do that:
- Screen size = actual size * zoom factor
- New screen size = screen size + ticks * pixels per tick
- New actual size = new screen size / zoom factor
The second problem is to switch modes when the object appears small on the screen. Instead of changing screen size by a fixed number of pixels per tick, you want to change by fewer pixels as the object gets smaller. This way you will approach zero but never actually reach it.
The key to solving this second problem is that idea of approaching a target without reaching it. In mathematical terms, that target – or limit – is an asymptote. We want to design an equation that asymptotically approaches zero as we scale down. But at the same time, we want to asymptotically approach a constant growth as we scale up. Here are the two asymptotes:
The first asymptote (y = 0) makes the size of our object to approach zero but never actually reach it. The second asymptote (y = x) makes the size of the object to increase by the same amount each time we tick the scroll wheel. Let y be the screen position, and all this happens relative to the zoom.
To design an equation with asymptotes, simply multiply to make each one a root of the equation:
The first factor represents the first asymptote (y = 0). The second represents the second (y = x). We just subtract one side from the other to turn the equation into a root (i.e. y = x, y – x = 0).
Study the equation
Plug this equation into Wolfram Alpha and it tells you that it is a pair of intersecting lines. We already knew that. We need to pull back from those two lines and see what happens. Let’s change the equation to this:
Now Wolfram Alpha tells us that it is a hyperbola. We can visually verify that it approaches zero on the left, and it approaches a 1-to-1 slope on the right.
We can also see that the equation crosses the y axis at 3. This is about where we “switch modes” from a big object to a little one. And finally, we can see the solution for y:
With a little work on the whiteboard, we can confirm that the numbers 3 and 36 are related to the arbitrarily chosen constant 9. The y intercept (3) is the square root, and the constant (36) is quadruple. With this knowledge, we can adjust the equation to “switch modes” at any screen size.
Complete the algorithm With a little more help from Steven Wolfram, we can find the inverse of this equation. With that, we turn this equation into an algorithm for determining the new size of an object:
- Screen size = actual size * zoom factor
- Starting x = (screen size^2 – small object^2) / screen size
- New x = starting x + ticks * pixels per tick
- New screen size = 1/2 (new x-sqrt(new x^2 + small object * 4))
- New actual size = new screen size / zoom factor
And with that we have one algorithm that operates one way on small objects, and another way on large objects. And it allows the user to fine tune by zooming in.
My partner’s comment I walked through this code with my partner, and explained how it solved three problems at once. His reaction was, “You used math where I would have used an if statement.” I think that was the most telling result of the whole exercise.
When we write code using discrete concepts like conditions, we apply brute force. We decide exactly where the solution changes from one mode to another. We force a corner into the solution. A singularity at which the behavior of the system changes violently. Bugs gather at such singularities. And those corners poke the user in the eye, even if he can’t put his finger on them.
But when we find one simple, continuous, elegant solution to many problems, we allow that solution to take its own form. It emerges naturally from the problem space itself. The benefit is not simply more beautiful code. It is also fewer bugs, and a more pleasant user experience. So when faced with a multitude of problems, put them all together and see if a single solution emerges.
Posted in Uncategorized | No Comments »
May 14th, 2010
I presented this material at Dallas XAML on May 4, 2010. Please download the demo code and follow along. This post takes us the rest of the way the project called “Step4”.
When we left the project in part 4, we had injected a layer between the model and the view. In the process, we broke updates. Now let’s fix them.
Add INotifyPropertyChanged to the view model Updates stopped working because we are now data binding to the view model. As of yet, the view model does not implement INotifyPropertyChanged. As I mentioned in part 2, there are really only two reasons to ever fire PropertyChanged:
- Something other than the view changes the property, or
- The property depends upon another property.
Neither WPF nor Silverlight need you to fire PropertyChanged for a property that the view itself is changing. The view already knows that it has changed. It’s the one that changed it!
The updates that failed are all related to dependent properties. DisplayAs depends upon first name, last name, and email. Title depends upon DisplayAs. We moved both of these properties into the view model. So the view model needs to know when these properties change.
Possible solution: interception Since the view model wraps the data model, it is possible to intercept changes on their way to the data model. The PersonViewModel understands what Title depends upon, and it intercepts all of those properties. It can fire the appropriate PropertyChanged events.
public string FirstName
{
get { return _person.FirstName; }
set { _person.FirstName = value; FirePropertyChanged("Title"); }
}
public string LastName
{
get { return _person.LastName; }
set { _person.LastName = value; FirePropertyChanged("Title"); }
}
public string Email
{
get { return _person.Email; }
set { _person.Email = value; FirePropertyChanged("Title"); }
}
public string Phone
{
get { return _person.Phone; }
set { _person.Phone = value; }
}
public DisplayStrategyViewModel DisplayAs
{
get { return new DisplayStrategyViewModel(_person, _person.DisplayAs); }
set { _person.DisplayAs = value.DisplayStrategy; FirePropertyChanged("Title"); }
}
When the user changes any of the properties that Title depends upon, the view model fires PropertyChanged for the Title. Title does not depend upon Phone, so that one doesn’t fire. The view model is keeping track of dependencies.
But what about the DisplayStrategyViewModel? It doesn’t intercept the properties that it depends upon. It needs to be notified when FirstName, LastName, or Email is changed. That means that PersonViewModel would have to send these notifications to DisplayStrategyViewModel. That’s too much coupling between the two. Let’s find a different solution.
Better solution: indirect notification
The view models already have something in common. They both know about the Person data model. Let’s allow them to communicate with each other through the data model.
When the PersonDataModel changes one of the properties on the Person object, the DisplayStrategyViewModel should hear about it. It needs to know about those property changes. We have, conveniently enough, a mechanism for listening to property changed events. Although INotifyPropertyChanged is intended for data binding, you can use it for your own internal notification as well.
Person already implements INotifyPropertyChanged. But it used to fire property changed events for dependent properties. Now we want to know about changes to the properties themselves. Let’s add those notifications.
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string FirstName
{
get { return _firstName; }
set { _firstName = value; FirePropertyChanged("FirstName"); }
}
public string LastName
{
get { return _lastName; }
set { _lastName = value; FirePropertyChanged("LastName"); }
}
public string Email
{
get { return _email; }
set { _email = value; FirePropertyChanged("Email"); }
}
public string Phone
{
get { return _phone; }
set { _phone = value; FirePropertyChanged("Phone"); }
}
}
Person no longer keeps track of dependent properties. That’s the view model’s job. Now it’s firing PropertyChanged events so that the view models can collaborate with each other. To close the loop, DisplayStrategyViewModel needs to listen.
public class DisplayStrategyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public DisplayStrategyViewModel(
Person person,
DisplayStrategy displayStrategy)
{
_person = person;
_displayStrategy = displayStrategy;
_person.PropertyChanged += PersonPropertyChanged;
}
private void PersonPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "FirstName" || e.PropertyName == "LastName" || e.PropertyName == "Email")
FirePropertyChanged("Display");
}
}
Now when the PersonViewModel changes one of the properties that DisplayStrategyViewModel depends upon, it can notify the view.
Best solution: automatic dependency management
Wouldn’t it be better if you didn’t have to write all of that dependency management code in the view model? You don’t, if you use Update Controls. But this isn’t an Update Controls presentation, so back to doing it manually.
Finish it out
To finish the transition from the interception solution to the notification solution, we remove the interceptors from the PersonViewModel. Instead, that view model also listens to changes.
public class PersonViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Person _person;
public PersonViewModel(Person person)
{
_person = person;
_person.PropertyChanged += PersonPropertyChanged;
}
void PersonPropertyChanged(object sender, PropertyChangedEventArgs e)
{
FirePropertyChanged("Title");
}
}
Now all of the view models listen to changes in the data model. If anything changes the data model, the view model will notify the view of changes to dependent properties. It’s like a publish-subscribe mechanism, but we’re using the data model itself. The data model has become the conduit for messages.
Next time, we’ll upgrade our application to manage a collection of people. In the process, we will dispel some of the myths about ObservableCollection.
Posted in Patterns | No Comments »
|