|
LATEST POSTS
Friday, October 12, 2007
Holiday in the Bahamas today but not in Canada. Which means I'm working while my daughter plays XBox behind me. So I'm going to vent my frustration on #regions. Maybe it's my alternative upbringing but I'm not a huge fan of #regions in Visual Studio. And here is one example why: As-is, this is pretty useless to me. My first order of action when I see this is Ctrl+M, then Ctrl+L to expand the entire thing which is another step I need to do to get my work done. "But Coding Hillbilly," you say, "doesn't it help to organize your code?" NAY, I say, NAY! If you are looking through a piece of code and see a method or property belonging to the Raccoon class, how do you go about looking for it? Do you analyze whether it is a property or method, then search for the class in Solution Explorer, then epxand the corresponding region and scroll through it? Of course not, you press Ctrl+B to zero in on it with ReSharper (and I'm pretty sure Visual Studio has a semi-functional alternative). The point is, you couldn't care less where the method is in the code. There are better ways to find methods. Ctrl+B, Ctrl+Alt+B, Alt+F7, Ctrl+Shift+F, Ctrl+F12, even skipping through the methods using Alt+Up and Alt+Down. In fact, I would argue that separating them into #regions like this actually makes it harder to scan through code because of the cases where some regions are expanded and others aren't. Using ReSharper also exacerbates this problem. A common refactoring scenario is Extract to Method (Ctrl+Alt+M, if memory serves). And ReSharper isn't going to bother searching through #regions to figure out where the method should be placed (nor should it). Ditto for when I start using method names that don't exist, then Alt+Enter my way through some NotImplementedException goodness. The #regions just get in my way. I've also been in places that are so dogmatic about them, there are actually empty #regions because the coding standard says you *have* to have one for private methods, even if you don't have any. And don't get me started on nested #regions. Admittedly, they do have their place. Prior to .NET 2.0, it was nice having the designer code hidden from view. Ditto for when you implement trivial interfaces that you rarely want to look at again. IConvertible, for example. So I suppose the real point is, don't overuse them. You aren't organizing anything by compartmentalizing properties and methods in this way. If you're one of those compulsive types, write some sort of add-in that alphabetizes the members or organizes them by how long they are or something. Just don't dump them into #regions. Kyle the Regional
Monday, October 01, 2007
One of my contracts involves what is essentially a document management system where users can search for legal documents based on metadata attributes as well as their contents. I've been working on it since before the days of SharePoint and even before Content Management Server was somewhat affordable. I mention this to cut off any comments that might say, "Why don't you just use a document management system?" It's in .NET and could benefit from migration to another platform. I'm not going to do it for reasons not worth mentioning. ACCEPT IT! There are two things worth posting about. Neither are new techniques by any stretch. The first is how to search the content and tie that into the metadata search. The second is how to restrict access to the documents (all in Word format) which are not restricted by IIS by default. Searching In order to search the contents of the repository, I'm using a technique that's been around for many moons. It starts with Microsoft Indexing Services (right-click My Computer, select Manage..., and it's under Services). I created a catalog and added the directory containing my documents. Easy enough. Now I have a catalog you can query from code using the Indexing Service query provider. I'd give you sample code but I don't have any; I don't query the catalog from code. It's no good to me on its own. I need to merge the results in with the metadata search in SQL Server. And rather than get one set of results from SQL Server and another from Indexing Services and merging them, I'm letting SQL Server do everything. To do this, I added a linked server to SQL Server: EXEC sp_addlinkedserver MyDocs, 'Index Server', 'MSIDXS', '<Indexing Service Catalog Name>' The first parameter can be whatever name you want. The next two are fixed and the last is the name of the Indexing Service catalog. From here, you can query the results directly from SQL Server and join them with any other query you want. For example: SELECT MetadataField1, MetadataField2, DocumentFileName FROM doc_metadata dl INNER JOIN OpenQuery( MyDocs, 'SELECT Filename FROM SCOPE( ) WHERE CONTAINS( Contents, ''<search term>'' ) ') q ON dl.DocumentFileName = q.Filename WHERE <filter conditions> And just like that, that squirrel's done, as my pappy used to say. The Indexing Service includes a bunch of properties you can return if you like, but for my poor man's document management system, all I need it for is to filter the list of search results. NOTE: Out of the box, the Indexing Service will index all forms of Office documents. For anything else, you'll need to find an appropriate filter that plugs into the service. Adobe makes one for PDFs but that's the extent of my knowledge. Restricting access to Word documents The application uses Forms Authentication to ensure you have appropriate access to the system. There are three roles: admin, subscriber, and guest (and an implied fourth role: ya can't git in). With the default forms authentication setup, anytime you navigate to an .aspx page, it will automatically redirect to the login page. This doesn't hold true for "unmapped" file extensions, such as image files or documents. By "mapped", I mean that IIS recognizes the extension and maps it to an ISAPI filter. Whenever IIS receives a request for a file with an extension it knows about, it filters the request through the corresponding ISAPI filter, which can then perform whatever funkiness it needs to before and/or after the request. Without getting too technical about the ASP.NET ISAPI filter (mostly 'cause I don't know too much about its inner workings including whether it actually *is* an ISAPI filter vs. an ISAPI application), I wanted any requests to Word documents to redirect to the login page if the user hadn't logged in. The same as any other page. So in IIS, I went to the application properties and added a mapping for .doc files (from the Virtual Directory tab, click Configuration..., then Mappings) to the ASP.NET executable (usually C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll or something akin). With that in place, users can no longer navigate directly to Word documents. Such requests will be re-directed to the login page. But there is one last requirement: guests aren't allowed to view documents. Easy enough: add a separate web.config to the documents folder restricting access only to subscribers and admins: <?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
<authorization>
<allow roles="subscriber, admin" />
<deny users="*" />
</authorization>
</system.web>
</configuration>
Note: You can also accomplish this with a <location> element in your application's main web.config file. I picked this method because I have a feeling I'll be moving the documents out to a separate virtual directory altogether very soon.
And that's that. I shan't go into the details of setting up your roles but as you may have guessed from the web.config, that is a requirement in order for this to work. It's pretty straightforward Forms Authentication stuff so Google should be able to help you out easily enough. If not, my door is always open.
So with these two techniques, I now have a method for users to search the contents of documents without allowing them to actually open them. Maybe I can get a job at www.experts-exchange.com.
Kyle the Restrictive
Tuesday, May 29, 2007
OK, so why hasn't anyone told me of the rockage that is ActiveRecord? I was halfway through writing my first NHibernate XML file when Oren himself pointed me to StoryVerse, whose code is easy enough to follow that I was able to pick up a practical use of ActiveRecord without too much hassle. Gives some context to the attribute vs. XML file debate which was passing me by. And I have to say, the thought of bypassing XML file management is pretty appealing for a hillbilly with the first signs of carpal tunnel. Data schema dependency be damned, that's some bit of magic when you can dump a bunch of attributes on a class and not write a single line of data access code. Which brings me to the real reason for this here hootenanny. One of my classes is called FiscalSystem. Its implementation is not worth explaining except for one of its properties. To wit: StateProvince a class which in turn has a property called Country. I.E. A FiscalSystem belongs to a State/Province which belongs to a Country. (I didn't do the reverse relationship because the app is small enough that I don't need it.) So to get a FiscalSystem's country: myFiscalSystem.State.Country.Name. Next comes the query: Show me all FiscalSystem objects that apply to a particular country. Here's the query I tried: ICriterion[] criteria = new ICriterion[]
{
Expression.Eq( "State.Country.Id", countryID )
};
FiscalSystem[] systems = ActiveRecordBase<FiscalSystem>. FindAll( criteria );
Side note: In reality, all I actually wanted on my FiscalSystem class were StateName and CountryName properties because I didn't need the semantics of a full State or Country object anywhere else. But I was unable to determine a way of having ActiveRecord look up information from more than one table (i.e. State and Country) for my FiscalSystem class. So I had to create objects for each of them.
No dice. The error is long gone now but it went something like "Could not resolve property name 'State.Country.Id'". Works if I query at the State level, but not at the Country level. Which leaves me to believe it has something to do with reflection.
Incidentally, you get a similar error if you try to bind a GridView to a list of FiscalSystem objects and use a BoundField that references State.Country.Id (or anything at the Country level). You get around that be using a TemplateField instead with Eval( "State.Country.Id" ).
My alternate solution was to dig into HQL which is amazing in its own right: SimpleQuery<FiscalSystem> query = new SimpleQuery<FiscalSystem>( @"
FROM FiscalSystem fs
WHERE fs.ParentState.ParentCountry.Id = ?
", countryID );
FiscalSystem[] fiscalSystems = (FiscalSystem[])ExecuteQuery( query );
Man, there is a price to be paid living out of the loop down in the caribbean all these years.
Kyle the Impressionable
Thursday, May 24, 2007
Some years ago, I built a commercial ASP.NET 1.1 application and part of it is about to be sold off. I've been the sole developer on it since its inception but haven't spent much time on it because it hasn't been a major focus for the person for whom I built it. Now, after being tasked with preparing a stripped down version of it that someone else will be looking at, I'm starting to realize just how far my development skills have come in the last few years. But that's not what I came to talk about. The application is small enough that I'm taking the opportunity to refactor a little as well as upgrade to .NET 2.0. Not using the upgrade wizard, mind you. Last thing I want in a .NET 2.0 app is a bunch of designer code intermingled with the page logic. And let's just say I wasn't exactly a proponent of MVC back then, let alone MVP. Heck, at the time I was just proud to be using the DAAB (the original version, with the static SqlHelper). One of the things I wanted to tackle was cleaning up the authentication code to take advantage of the ASP.NET security controls. In the original version, I used what I think was fairly standard practice at the time. Namely, in the AuthenticateRequest event in Global.asax, I created a GenericPrincipal using the username and roles from the application's database. (Oh yeah, I'm using Forms Authentication.) This was pretty useful in creating a guest-only version of the app as well as an admin section. With .NET 2.0, I had read and played with the Membership and Role Providers but hadn't done any real work with it other than to run through the ASP.NET configuration wizard and stare blankly at the sheer number of tables created to support it. In this version, I had two goals: a) use those providers, and b) do NOT, under any circumstances, create that aspnet_regsql security schema in my database. After all, I already had a user/role schema set up that worked. So starting from Scott Guthrie's comprehensive list of ASP.NET security resources, I began wading through tutorials each of which wants you to either use that same schema, or use some entirely different backend. All I really wanted was to use the SQL providers with my own schema. It was this article that got me on the right track. From there I was able to determine what I had to do. To wit, I created a custom membership provider and a custom role provider to do only what I need them to do. The membership provider derives from SqlMembershipProvider and contains only the following code: public override bool ValidateUser( string username, string password )
{
string role = MyDal.ValidateUser( username, password );
if ( role == null )
{
return false;
}
return true;
}
Similarly, the role provider derives from SqlRoleProvider and contains the following: public override string[] GetRolesForUser( string username )
{
ArrayList roleList = new ArrayList( );
DataTable roleTable = MyDal.GetRolesForUser( username );
foreach ( DataRow row in roleTable.Rows )
{
roleList.Add( row["UserGroup"].ToString( ) );
}
return (string[])roleList.ToArray( typeof( string ));
}
(NOTE: If I may use my Hillbilly mind powers for a moment: You WILL ignore the data access code.)
These methods do exactly what they say they do but they are doing it the way *I* want them to do it. I.E. They're retrieving the data from my database using stored procedures of my own design.
After that, it was a simple matter of registering the providers in the web.config: <membership
defaultProvider="MySqlMembershipProvider" >
<providers>
<add name="MySqlMembershipProvider"
type="MySqlMembershipProvider"
requiresQuestionAndAnswer="false"
connectionStringName="MyConnection" />
</providers>
</membership>
<roleManager enabled="true"
defaultProvider="MySqlRoleProvider">
<providers>
<add name="MySqlRoleProvider"
type="MySqlRoleProvider"
connectionStringName="MyConnection" />
</providers>
</roleManager>
Where "MyConnection" refers to an entry in the <connectionStrings> section.
Now I'm able to use the ASP.NET login control and the various menu and navigation controls using security trimming as well as protect my application without having to fiddle around with the forms authentication cookie.
Note that this works splendidly for my scenario. In my application, users log in and the system determines what role (that's right, I'm supporting only a single role, you wanna make something out of it?) they're in. That's it. There is no "I haven't registered yet so I'll just sign up online" or "I forgot my password can you send it to me?" or "What users are in this role?" scenarios. That's why I can get away with overriding only the two methods listed above.
Like JP says, small victories...
Kyle the Victorious
P.S. I still hate security-related coding
Monday, May 07, 2007
Apologies in advance if I've misspelled aspnet_compiler as aspnet_compilter anywhere in this post. For the life of me, I can't figure out why my fingers always want to add that 't'. Interesting little quirk. Many moons ago, I installed Scott Guthrie et al's Web Application Project, probably because I needed to for dasBlog to compile. Since then, I've kind of ignored it in my project list. Today I absent-mindedly added one to my solution thinking it was a regular VS2005 Website and I received errors in the build step that used aspnet_compiler to precompile the app: Could not load type 'SampleWebPage.aspx.cs'. I set up debugging this already knowing in the back of my mind that it probably had something to do with the type of web project it was. My first clue was that when I built the web project in Visual Studio itself, it was compiled using csc: Whereas the output for a Web Site build is as follows: The other hint was the property pages. For a Web Application Project, you get property pages that look like they do for a class library or WinForms app: For a Web Site: Plus they're called different things in the right-click menu (Properties vs. Property Pages). Finally, there's the actual error message itself. Web Applications Projects, of course, use an explicit namespace, Web Sites do not. And the big ol' honkin' namespace declaration at the top of the class was a pretty solid indication that this ain't no VS2005 Web Site. So the choices were pretty obvious: modify the build to accommodate the Web Application Project or use a Web Site. I chose two as the path of least resistance since I had but one (incomplete) page in it. Kyle the Rebuilt
Saturday, April 14, 2007
Since none of you responded to the challenge implied at the end of my last post (the fact that it was made a mere four hours ago
notwithstanding), I have foregone billable work in favour of seeing if I could
do it myself. When installed (instructions below), this will add a Class Library Wizard template to your C# project templates list. When selected, it brings up a small form asking for the name of the first class, then creates said class in a class library project. InstallationIt's not as smooth as it could be. In order to display a form, I needed to create an assembly that has to be installed in the GAC (or some other location in the Visual Studio folder but I don't remember where). And as far as I can tell, .vsi files won't install things to the GAC. Here are the steps: - Run the .vsi file to install the project template. It won't work until the other file is installed in the GAC. You'll get a warning during the installation that it might not be safe. HEED THAT WARNI--er...I mean, it should be fine.
- Install Suvius.ClassWizard.dll into the GAC: gacutil -I <path to assembly>
Issues- It should complain if you enter a blank class name but I forgot to test
- Not sure what happens if you enter a name with illegal characters
- It really should have a cancel button
- I haven't tested this on a machine that is not my own
I wouldn't mind extending it so that it creates two projects: a class library and a test project. The test project would have a reference to NUnit and Rhino.Mocks and both projects would contain a class with the specified name although in the test project, it would have Test appended to the name. It would also be nice to be able to create the class using the ReSharper template just in case someone has modified it, rather than using a hard-coded, and very spartan, one. There is much more that could be done and luckily, it ties in with the Tree Surgeon app that Bil Simser was nice enough to let me join so hopefully this little side project won't fall by the wayside as they are prone to do. (That will require CodePlex to fix their little server crash problem but I'm optimistic.) Source code is available on request for the moment. I'll probably post it in the coming days when I rap about what it was like implementing the IWizard interface to build this sucker. Suvius.ClassWizard.dll (20 KB)ClassLibraryWizard.vsi (2.94 KB)
Saturday, April 14, 2007
One side effect of TDD seems to be a moderate to gargantuan increase in the number of projects in one's solution. And with almost half of those being Test projects, it was with no small amount of gratitude that I tracked down Steven Rockarts' post on creating a project template with references to NUnit and Rhino.Mocks built-in. One of the things Steven did when creating his template is something that has become automatic for most of us. To wit, the first step in creating a class library is to delete Class1. So after creating my project template (which by the way, required me to go to Tools | Customize, then the Commands tab, File menu to add the Export Template... menu item to the IDE first), I got to thinking that maybe I'd create my own Class Library template which doesn't include Class1. But being a forgetful Hillbilly, I would inevitably forget to use my template the first couple of times and creating a class library would take longer as I'd have to go through the extra stages of deleting my new project, cursing, then creating a new one with my template. Better to modify the master. Which on my machine, is located at: C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\ProjectTemplatesCache\CSharp\Windows\1033\ClassLibrary.zip Once there, I did the following: - Deleted class1.cs
- Opened classlibrary.csproj in a text editor and deleted the <Compile> element that referenced Class1.cs in the last <ItemGroup> element
- Opened csclasslibrary.vstemplate in a text editor and deleted the <ProjectItem> that referenced Class1.cs in the <Project> element at the bottom
Now my class libraries are created free of Class1. In addition to this, I also went to C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\ProjectTemplates\CSharp\Windows\1033 and made the same modifications to the contents of the ClassLibrary.zip file. I'm not 100% certain but I think this is necessary if you ever need to re-install the VS templates (using devenv.exe /installvstemplates), which I did have to do earlier today when my Web User Control template stopped appearing in my list. In an even more ideal world, creating a class library would automatically prompt you for the name of your first class, doing nothing if you leave it blank. Haven't found a feature like that in ReSharper but I can't imagine this isn't possible in some third-party tool somewhere.
Friday, March 30, 2007
Despite my strict mantra, "I don't do websites", here I am working on a plain old website. But thanks to www.templatemonster.com and ASP.NET master pages, it's going pretty quickly. But here's an odd thing with master pages. I have nested master pages and when I get to a content page (or another master page) that's two levels deep or higher, VS2005 no longer recognizes the <asp> controls in my HTML. They work just fine but the IDE marks them as errors ("not a known element") and they don't show up in intellisense... ...unless I open the master page(s) in the IDE (doesn't matter if it's design mode or source mode). In which case, all is right with the world again.
Friday, February 09, 2007
Finally! I'm part of the elite! Part of the clique who can now recommend Lutz Roeder's Reflector with full knowledge of its greatness. Until today, I was just a poser. I knew of Reflector and was keenly aware of what it did. I played with it for a bit but had never in my consulting career come across an instance where I needed to dive that deeply into someone else's code. But I left it installed on my machine because I wanted to pretend to be one of the big boys. And I started to crave the appreciative nods I'd get from better developers when they'd see it prominently displayed on my desktop. Then lo! An e-mail comes screaming through my Inbox: where is this error message generated for the Franken-app you built that syncs PalmPilot data with Livelink? (Side note: If I can brag a little, I'm actually kind of proud of that app. Built in .NET, it works better than the application I was trying to mimic on the old software Livelink was replacing.) The app uses Livelink's API to wedge stuff into the system so after a quick look through Livelink's code base itself to see if the error was there, I dusted off Reflector and pointed it at the API assembly. String search and lo, again! There it is! So now I can accurately answer that e-mail: It is generated in the receiveOpenResponse method of the LLConnectRouter object in LAPI_NETp.dll. That should get them off my back, yesno?
Tuesday, December 12, 2006
Here's one for you .NET Livelink developers. I love Christopher Meyer's EasyLAPI library from the Open Text Knowledge Centre. But of course, it's in Java and the Coding Hillbilly is strictly caffeine-free these days on account of his cholesterol.
The library was manageable enough that it didn't take too long to create a C# version. Some minor syntax changes and some other changes based on how I would do things if I were to write it from scratch in C# (most notable, replacing the Vector references with ArrayLists), and away I went.
Of course, that doesn't help you, the throngs of C# LAPI developers, in my readership. So I cleaned it up further, converted the comments to be .NET-like, and changed a couple of methods to properties as I saw fit. Resulting library is attached.
One fairly major change is the addition of a configuration handler for the sessions. The Java version used an .ini file to manage multiple sessions. I'm using a custom config section. The upshot is that you should include something like this in your app.config:
<LLSessions> <Session name="local" server="localhost" port="2099" username="Admin" password="livelink" /> <Session name="dev" server="localhost" port="2099" username="Admin" password="lldev@dm1n" /> <Session name="test" server="localhost" port="2099" username="Admin" password="lltst@dm1n" /> <Session name="prod" server="localhost" port="2099" username="Admin" password="llprd@dm1n" /> </LLSessions>
Then in your code:
llSession = SessionProperties.getSession( "test" );
In my code, I also add a defaultSession key to my app settings which allows me to switch environments quickly and without having to re-compile the code. Ideally, I should have a defaultSession attribute on the LLSessions node but I haven't got that far yet. Also on tap is the ability to encrypt username and password. Wouldn't mind using the Enterprise Library for configuration as well but then I get into problems of having one version for .NET 1.1 and another for .NET 2.0 (and perhaps 3.0 but I doubt it). Plus I don't want to force people to use the Enterprise Library if they don't wanna.
It has been tested very little except to create some folders and some RM objects. The rest of the code is lifted directly from Christopher Meyer's version which seems to be pretty solid to my limited understanding of Java and LAPI in general. The commenting took more time than the conversion. EasyLapi.NET.zip (308.23 KB)
Thursday, December 07, 2006
So I boot up my laptop and start pounding away at my Windows app, making changes that will add so much value that it's dripping from my screen (waitaminute...that's not value...EWWWW!). It should be noted that I'm the sole proprietor of this application on the basis that I'm the sole .NET developer on the team.
Hit F5 to let 'er rip and BAM! at the section that loads my custom config section from app.config, code I wrote almost by rote several weeks ago and which I've promptly forgotten because I haven't touched it since.
My custom config is pretty straightforward. Here's the app.config:
<configSections> <section name="Handlers" type="CodingHillbilly.SkinningManager.Windows.Configuration.MyConfigurationHandler, CodingHillbilly.SkinningManager.Windows" /> </configSections>
<Handlers> <Handler name="Raccoon" assemblyName="CodingHillbilly.Handlers.Raccoon" className="CodingHillbilly.Handlers.Raccoon.RaccoonHandler"> </Handler> <Handler name="Squirrel" assemblyName="CodingHillbilly.Handlers.Squirrel" className="CodingHillbilly.Handlers.Squirrel.SquirrelHandler"> </Handler> <Handler name="Possum" assemblyName="CodingHillbilly.Handlers.Possum" className="CodingHillbilly.Handlers.Possum.PossumHandler"> </Handler> </Handlers>
In the ConfigurationHandler, I create an object for each handler, gather them up in a Hashtable and return it:
public class MyConfigurationHandler : IConfigurationSectionHandler { public object Create( object parent, object configContext, XmlNode section ) { Hashtable skinninHandlers = new Hashtable( );
foreach ( XmlNode handler in section.ChildNodes ) {
if ( handler.NodeType != XmlNodeType.CDATA && handler.NodeType != XmlNodeType.Whitespace ) { string displayName = handler.Attributes["name"].Value; string assemblyName = handler.Attributes["assemblyName"].Value; string className = handler.Attributes["className"].Value;
MyHandlerObject handlerConfig = new MyHandlerObject( displayName, assemblyName, className );
skinninHandlers.Add( displayName, handlerConfig ); } }
return skinninHandlers; } }
And in the main code:
handlers = (Hashtable) ConfigurationSettings.GetConfig( "Handlers" ); foreach ( MyHandlerObject handler in handlers.Values ) { this.comboBoxHandlers.Items.Add( handler.Name ); }
Like I said, I wrote this long ago in a different time and for whatever reason, it starts complaining this morning. Whining that it can't cast the objects in handlers.Values to a MyHandlerObject anymore. But when I debug, every object in handlers.Values is, in fact, a CodingHillbilly.SkinningManager.Configuration.MyHandlerObject object.
The key was in the type attribute of the Handlers section. Yesterday, I handed off my work in progress to someone to start testing and in a fit of user-friendliness, I didn't want the executable to be named CodingHillbilly.SkinningManager.Windows.exe, so I changed the name of the assembly in the project properties to UltimateSkinner.exe.
Hence, it was creating handlers based on the old assembly and trying to cast them to handlers from the new assembly because the Debug folder still actually contained the old CodingHillbilly.SkinningManager.Windows.exe. I suspected there was some reference issue so I cleared the Debug folder completely, re-compiled and lo! Now it can't find a reference to the assembly to even create the config handler.
From there it was a quick fix to the <section> to reference the correct assembly:
<section name="Handlers" type="CodingHillbilly.SkinningManager.Windows.Configuration.MyConfigurationHandler, UltimateSkinner" />
So the moral of the story is: don't do anything to make the user's life easier.
moo
Thursday, December 07, 2006
I'm loving the Feature Browser. It's a feature in their designer where, rather than scrolling through a daunting list of properties for the grids, views, columns, etc., they give you a functional list, such as Editting, Grouping, Sorting, etc. So if you want to allow grouping, say, you select the Grouping node and it gives you a list of properties related to Grouping. Basically a filtered view of the properties that is much more manageable and even comes with context-sensitive help as well as a preview window of how your setting will affect your grid. It's my main view now and I'm hip to its jive. The other thing is a little idiosyncracy. In one of my grids, there was a + next to each item even though I had explicitly disabled grouping. The grid was read only and nothing happened when you clicked the +. Was kind of frustrating trying to get rid of it. Turns out, XtraGrid recognized that one of the properties on the objects I was binding to was itself a collection and it added the + thinking I may want to show this collection of objects. I didn't and the solution was to turn off the EnableMasterViewMode setting (conveniently located in the Master-Detail section of the Feature Browser). Anyway, after pretty heavy use of the thing over the last couple of weeks, I'll give it four possum-skins outta five. Well worth the price, even if I did shell out extra for source code I'll never, ever look at. moo
Saturday, December 02, 2006
Minor config issues this morning that warrant discussion. I had imported Scott Hanselman's font and colour settings because I wanted to see what all the fuss was about with coding on a terminal. Thanks, Scott, for providing those because I wouldn't have had the patience to go through the settings myself. Initial thoughts on that before continuing. The light on dark thing is kinda cool. Not entirely sure it's easier than dark on light because it's a little jarring whenever I switch to VS from every other application I work with. Still need to do some tweaking of the ReSharper settings but I'll keep it like this for a while. I do find that I'm able to work in a smaller font comfortably, though. And I can't believe I waited this long before discovering Shift+Alt+Enter = Full Screen (which is MUCH easier to use after coming across Steven Rockart's list of shortcuts). Next: The Inconsolata font. That lasted about fourteen seconds. Judging from the comments on the page where I discovered it, I am apparently the only one who notices the little pixel sticking out along the bottom of the a's, e's, o's, etc. It's even more noticeable as you make the font smaller. Not sure why that was the first thing that jumped out at me because I'm not generally that observant. But once I noticed it, I couldn't focus on anything else. Still, it looks like the font is a work in progress. I'll monitor the progress but for now, I'll stick with Consolas. Start of my configuration issues: The first time I compiled a project with the new settings, the output window (which *really* looks weird with light on dark) spewed gibberish. Can't quite explain that and I'm afraid I didn't take a screenshot but trust me. It wasn't even a proper language. Weird thing: I cut and pasted to Notepad and it was fine. So I changed the font settings for the output window to Consolas (it was set at Inconsolata) and the problem went away. Switched the font back to Inconsolata (curiosity, y'know) and the problem didn't return. Hence, I can't be more specific than: it was gibberish. Next problem: My toolbox icons. They were all replaced to one of two icons. The first was the usual icon you see for controls that are in your project. The second, more prevalent one was an icon I have never seen before. Again, no screenshot because frankly, I was more concerned with fixing the problem than entertaining you people with pretty pictures. Solution: Export my settings, then re-import them. I've heard tell that resetting your toolbox will do the same but I've added a vatload of the DexExpress editors and didn't much feel like redoing it. Re-importing worked except... A few ReSharper shortcut keys couldn't be rebound. Looks like the ones that overrode existing Global shortcuts. Solution: Re-install ReSharper (assuming you haven't customized your ReSharper shortcuts). So I'm back to where I was yesterday but a little wiser and a little darker. Moo
Thursday, November 30, 2006
Given my recent activities, I've decided to adopt the Native American name, Travels with XTraGrid. And on that note, a couple of little tidbits about the grid which, like me, is powerful but quirky. On the whole, it is very easy to bind collections of objects to the grid. There are two things I discovered about it that I believe apply to both the XtraGrid and the regular Windows DataGrid/GridView: - In order to be able to add or remove items, your collection must implement IBindingList. This is in the documentation and there is sample code to do this so I won't dwell on it.
- You CAN bind to a collection of primitives (ints, doubles, strings (yes, yes, I know, work with me here)). But as far as I can tell, you can't do it so that you can add to or remove from the collection nor can you make existing ones editable. You need to use a collection of objects.
Your options for getting around the last point are: a) create a MyDouble or MyString class with a Value parameter and bind the grid to a collection of those, or b) Use an unbound grid and update your collection manually using the grid's various events. Neither solution is very clean in that you eventually have to write code that the next developer is going to look at and say, "What the hell was he thinking?" and proceed to re-write using something that is likely equally cumbersome. While I'm on the topic of DevExpress control idiosyncracies, keep an eye on your NavControls. I've found that they don't clean up nicely if you add/remove items to them or try cutting and pasting between two or more of them. You'll find a lot of leftover NavBarItems and NavBarGroup items left over in your Designer.cs file that you have to remove manually. My way of countering: make sure you change the name of all of the controls, including the groups and items. Much easier to zero in on navBarItem3 when the rest of your controls aren't named navBarGroup1, navBarItem5, etc.
Monday, November 27, 2006
Let's say you're building a wizard to create a project. On the first page, you specify the project name and date. On the second, you select from a list of available tasks the ones you want to apply to the project. On the third page, you enter some more data. And so on and so forth. Now let's say you want to edit the list of tasks that apply to the project. We go back to the wizard, remove some tasks, add others, and click Finish. To summarize what I would like to accomplish: I want the ability to go back to this wizard, edit the list of tasks, and record only the changes to the database. I.E. I can't use DELETE FROM ProjectTasks WHERE ProjectID = @projectID, then re-insert the user's new selections. The reason for this is, as you suspect, because this is a simplified version of my problem. So just assume that I can't wipe the slate clean like this. When any changes are made, I need to add the new Tasks to ProjectTask and delete any that were deselected. Now, I'm all over this if I were using DataSets, DataTables, and DataRows. Been doing it for years with the RowState property. Attach select, insert, update, and delete commands to your adapter/SqlHelper/whatever, run UpdateDataSet, and watch the magic. This time around, though, I'm using a domain model. And at the moment, my Project item has as its properties: ID, Name, Date, and Tasks. My Tasks property is of type Dictionary<int, ProjectTask> where the key is the TaskID. ProjectTask has properties: ID, Name, DateAdded, Status. Status is an Enum which basically does what the RowState property did. I use it in the UI to record whether the item was added/deleted/left alone. The data access layer uses it to figure out which command to call. By the way, the changes have to be done as a batch like this because of how wizards work. Users can click Finish at any time or Cancel at any time. I can't start deleting or adding things as soon as the user selects/deselects a Task because the user may click cancel on the next page. The entire Project update must happen when Finish is clicked. So essentially, I have the DataSet model transported to objects and I'm wondering if there is a better way. I'll bet the life of someone I'm at least mildly fond of that there is at least one pattern to handle exactly this so if someone wants to respond with just the name of that pattern, I don't mind doing the research on my own. In the meantime, I'm offline as I type this and I will see if I can find it on my own as soon as I return to the connected world. In return, I'll probably post my wizard framework code shortly because it's actually not bad if you don't mind a bit of hillbilly boasting. Not nearly as easy to use as the one I bought from DivElements but I wanted to make mine look a little less Windows 95 than the standard wizard.
Sunday, July 23, 2006
Re: The leftover error from my last post. I did spend a little time mucking around with Regmon but other than a few missing keys that meant nothing to me, I didn't find anything. Then I opened SQL Server Management Studio and got the same error which was a big “uh oh” moment. I quickly realized I was out of my depth and re-installed Visual Studio 2005 so all is well for the moment. But that's not what I came to talk about.
In my last post, I mentioned an economic model posing as a spreadsheet. I also mentioned some VBA behind the scenes. The VBA doesn't do a whole lot; most of the work is done by Excel as it should be. There's a form with some buttons on it and the main button will “process” the model. Which is to say it copies the contents of some cells from one place to another. There's a lot of busy work to support that but at its core, that's what it does. And processing the model takes, on average, about four seconds. Not too bad considering what it's doing and most people would consider that good enough...
I also have a separate .NET application that makes use of this model. One of its functions is to automate the processing of the model for various dimensions. Among the dimensions: up to four different price levels, up to three cost levels, up to eight field scenarios and up to 500 sets of fiscal terms. Let me save you the math: that's 48,000 executions of the model to generate the entire set of results. So all of a sudden, my decent-looking execution time of four seconds translates to over two days straight of calculations. That doesn't include the time it takes to store the results in a database which brings it closer to three days.
And let's talk about storing the results. There are a lot of them. On average, about 300 - 400 individual measures (i.e. cell values) to store for each individual calculation. In my first pass at the application, I controlled everything from the .NET application. It was essentially four nested loops, one for each dimension. And two of the dimensions (the field scenarios and the fiscal terms) involved opening a separate spreadsheet and importing it into the model before processing. After processing, I iterated through the results sheet and picked out the individual cell values to store in the database.
This is INCREDIBLY time-consuming. A single run (one price, cost, field scenario and fiscal term set) took around forty-five seconds. Again, I'm no Interop specialist but from my own experience, retrieving the value of a cell in Excel is &*$% murder on a CPU. If I had to make something up about what is going on, I'd say it's because you have to cross COM boundaries for each cell retrieval but frankly, I'm not quite sure what that means. I've heard the words spoken before and they seem to apply here so that's my official technical summary.
To get around this, I had to sell a little bit of my soul and move code out of C# and into VBA. The application still controls importing the (up to) eight field scenarios and the (up to) 500 fiscal terms but everything else is done in VBA: setting the price, setting the cost level, processing the model and storing the results to the database. Net execution time dropped from forty-five seconds to 'twixt five and six on average.
I was ecstatic about the performance gain but this really sucks for more than just the obvious reasons. It means I now have this external application's code in two places. And one of those places is an Excel spreadsheet that can be (and has been) zipped up and e-mailed anywhere in the world regardless of whether someone uses the external app or not. It means I have a lot more work to do if I ever need to update the external application. It means I have a bigger job ahead of me when a decent alternative to VBA comes out. But as long as the model resides in Excel (and there is no business justification to moving it to anything else; let's face it, Excel rocks at this kind of thing), I'm stuck with VBA.
On a semi-related closing note: Every once in a while, my reference to the Excel interop library gets lost. I know now that this is because of the registry problems I had yesterday but I still don't know what causes it. Anyway, when this problem happens, I have to reset the reference to the Excel library and the namespace changes from Microsoft.Core.Interop.Excel to just plain Excel. It seems Microsoft.Core.Interop.Excel is the namespace for the PIA and Excel is the namespace for the plain old IA. So when I lost access to my PIA and re-added the reference to the Excel library, instead I got a reference to the IA. When I fixed the PIA problem and reset the reference to the Excel library, I got my Microsoft.Core.Interop.Excel namespace back.
Can I get a Hoo-ah! for the Coding Hillbilly?
Disclaimer
The opinions expressed herein are my own personal opinions and do not represent
my employer's view in any way.
Copyright © 2008 Kyle Baley. All rights reserved.
|
|
|
LATEST POSTS
POPULAR POSTS
LINKS
BLOG ROLL
|
|
CATEGORIES
ARCHIVE
| December, 2007 (8) |
| November, 2007 (8) |
| October, 2007 (23) |
| September, 2007 (15) |
| August, 2007 (8) |
| July, 2007 (6) |
| June, 2007 (11) |
| May, 2007 (19) |
| April, 2007 (14) |
| March, 2007 (3) |
| February, 2007 (4) |
| January, 2007 (7) |
| December, 2006 (5) |
| November, 2006 (9) |
| October, 2006 (11) |
| September, 2006 (14) |
| August, 2006 (11) |
| July, 2006 (15) |
| June, 2006 (8) |
| May, 2006 (10) |
| April, 2006 (12) |
| March, 2006 (3) |
| February, 2006 (7) |
|
|
|