Tuesday, 22 December 2009

Changing the last item in a repeater

You can't change the last item of a repeater in its itemDataBound event, as there is no way of knowing you are on the last item. The concept you need to follow is that you poke it later on during the preRender event of either the repeater or the page.

I needed to do this, but I had an extra complication - I was working with a nested repeater in a page that had a master page. The fact that this was a child page made finding my control a bit of a pain using Page preRender, so I thought it would be sensible to use the preRender event of the parent repeater. To my surprise while debugging I discovered that my parent repeater's preRender event was firing before the child or nested repeaters had even started binding. Weird.

So, in the end, I used the child repeater's preRender event. In this I look for the parent repeater, and loop through its repeater items looking for my nested repeater (which I'm about to render). Then I find my control and make the change I want.

Simples!

protected void ChildRepeater_PreRender(object sender, System.EventArgs e)
{

   Repeater ParentRepeaterRpt = (Repeater)ContainerofParent.FindControl("ParentRepeater");

    foreach (RepeaterItem item in ParentRepeaterRpt.Items)
    {
        Repeater ChildRepeaterRpt = (Repeater)item.FindControl("ChildRepeater");
        if (ChildRepeaterRpt.Items.Count > 0)
        {
            RepeaterItem rptItem = ChildRepeaterRpt.Items[ChildRepeaterRpt.Items.Count - 1];

            Literal MyLit = (Literal)rptItem.FindControl("MyLitName");
            MyLit.Text = "whatever";
        }
    }



}

Thursday, 17 December 2009

Directory.Move...but no Directory.Copy?

If you want to move a directory and its contents in .Net then you can use this:
Directory.Move(sourceDir,destDir)
For some reason unbeknownst to me, there is no Directory.Copy. So, instead of re-inventing the wheel I googled for some code that's already written. I found this, and it seems to work a treat. It was posted by a chap called Mike on the vbCity forums, so all gratitude should go to him:

' Usage:
    ' Copy Recursive with Overwrite if exists.
    ' RecursiveDirectoryCopy("C:\Data", "D:\Data", True, True)
    ' Copy Recursive without Overwriting.
    ' RecursiveDirectoryCopy("C:\Data", "D:\Data", True, False)
    ' Copy this directory Only. Overwrite if exists.
    ' RecursiveDirectoryCopy("C:\Data", "D:\Data", False, True)
    ' Copy this directory only without overwriting.
    ' RecursiveDirectoryCopy("C:\Data", "D:\Data", False, False)

    ' Recursively copy all files and subdirectories from the specified source to the specified 
    ' destination.
    Private Sub RecursiveDirectoryCopy(ByVal sourceDir As String, ByVal destDir As String, ByVal fRecursive As Boolean, ByVal overWrite As Boolean)
        Dim sDir As String
        Dim dDirInfo As IO.DirectoryInfo
        Dim sDirInfo As IO.DirectoryInfo
        Dim sFile As String
        Dim sFileInfo As IO.FileInfo
        Dim dFileInfo As IO.FileInfo
        ' Add trailing separators to the supplied paths if they don't exist.
        If Not sourceDir.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString()) Then
            sourceDir &= System.IO.Path.DirectorySeparatorChar
        End If
        If Not destDir.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString()) Then
            destDir &= System.IO.Path.DirectorySeparatorChar
        End If
        'If destination directory does not exist, create it.
        dDirInfo = New System.IO.DirectoryInfo(destDir)
        If dDirInfo.Exists = False Then dDirInfo.Create()
        dDirInfo = Nothing
        ' Recursive switch to continue drilling down into directory structure.
        If fRecursive Then
            ' Get a list of directories from the current parent.
            For Each sDir In System.IO.Directory.GetDirectories(sourceDir)
                sDirInfo = New System.IO.DirectoryInfo(sDir)
                dDirInfo = New System.IO.DirectoryInfo(destDir & sDirInfo.Name)
                ' Create the directory if it does not exist.
                If dDirInfo.Exists = False Then dDirInfo.Create()
                ' Since we are in recursive mode, copy the children also
                RecursiveDirectoryCopy(sDirInfo.FullName, dDirInfo.FullName, fRecursive, overWrite)
                sDirInfo = Nothing
                dDirInfo = Nothing
            Next
        End If
        ' Get the files from the current parent.
        For Each sFile In System.IO.Directory.GetFiles(sourceDir)
            sFileInfo = New System.IO.FileInfo(sFile)
            dFileInfo = New System.IO.FileInfo(Replace(sFile, sourceDir, destDir))
            'If File does not exist. Copy.
            If dFileInfo.Exists = False Then
                sFileInfo.CopyTo(dFileInfo.FullName, overWrite)
            Else
                'If file exists and is the same length (size). Skip.
                'If file exists and is of different Length (size) and overwrite = True. Copy
                If sFileInfo.Length <> dFileInfo.Length AndAlso overWrite Then
                    sFileInfo.CopyTo(dFileInfo.FullName, overWrite)
                    'If file exists and is of different Length (size) and overwrite = False. Skip
                ElseIf sFileInfo.Length <> dFileInfo.Length AndAlso Not overWrite Then
                    'Debug.WriteLine(sFileInfo.FullName & " Not copied.")
                End If
            End If
            sFileInfo = Nothing
            dFileInfo = Nothing
        Next
    End Sub

Wednesday, 16 December 2009

Check if a record exists in a stored procedure

I have users assigning selected items from a list to a group that they have created. I don't want to bother the user with detailed information about whether any particular item they selected was already in the group, I just want to add it if it's not there, and do nothing if it's there already.

The neatest way to do this was to drop the logic into the stored procedure like below. The 'IF NOT EXISTS' condition means I can insert only when there is no record matching the SELECT query.

IF NOT EXISTS(SELECT * from [table] where something = @mySomething)
 
BEGIN
 
/**** do insert ****/
   
END

GO

Wednesday, 9 December 2009

Validating input for an email address

Use this pretty, simple, and pretty simple regular expression validator:
<asp:RegularExpressionValidator ID="valEmailAddress" ControlToValidate="EmailTextBox" ValidationExpression=".*@.*\..*" ErrorMessage="Email address is invalid" EnableClientScript="true" runat="server" ValidationGroup="Insert">*</asp:RegularExpressionValidator>

Or, if you want to do it in code rather than use a control, try this:

public static bool isEmail(string inputEmail)
{
    if( inputEmail == null || inputEmail.Length == 0 )
    {
        throw new ArgumentNullException( "inputEmail" );
    }

    const string expression = @"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}" +
                              @"\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\" +
                              @".)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$";

    Regex regex = new Regex(expression);
    return regex.IsMatch(inputEmail);
}

Monday, 23 November 2009

Accessing an output paramater from a stored procedure - getting the id of an inserted row

What I want to do is access the id of the row I just inserted into the database. To do this I use the itemInserted event of the datasource.

Here I'm inserting using a SqlDataSource configured with the following output parameter within the InsertParameters tags:

<InsertParameters>
<asp:parameter direction="Output" name="answerID" type="Int32"></asp:parameter>
</InsertParameters>
and here's the inserted event handler
Protected Sub answerInserted(ByVal sender As Object, ByVal e As SqlDataSourceStatusEventArgs) Handles InsertAnswerDS.Inserted
theValueIWant = Convert.ToInt32(e.Command.Parameters("@answerID").Value)
End Sub
And what's going on in the stored procedure? It looks like this:
CREATE PROCEDURE [dbo].[InsertAnswer]
@questionID integer,
@answerText varchar(50),
@answerID integer output

AS
BEGIN

INSERT INTO [Answers] (
[questionID],
[answerText]
)

VALUES 
(
@questionID,
@answerText
)

END

SELECT SCOPE_IDENTITY()
Set @answerID = scope_identity()

Thursday, 12 November 2009

Zip up a directory and download to client

I googled about this and found some pretty contrived ways of doing it, especially when it came to honouring subdirectories and their structure. Eventually I discovered the FastZip class of the ICSharpCode.SharpZipLib.Zip library that makes it very easy.

And here's my code. Send it the path of the dir to be zipped and the desired name of the resulting zip file:

Public Sub ZipAndDownload(ByVal strPath As String, ByVal strFileName As String)

HttpContext.Current.Response.ContentType = "application/octet-stream"
HttpContext.Current.Response.AddHeader("Content-Disposition", "attachment; filename=" & strFileName & ".zip")

'FastZip - zip all the file and folders
'and stream it through the Response OutputStream
Dim fz As New ICSharpCode.SharpZipLib.Zip.FastZip()
fz.CreateZip(HttpContext.Current.Response.OutputStream, strPath, True, Nothing, Nothing)

End Sub

Tuesday, 3 November 2009

Using the Ajax HTMLEditor 'Lite top toolbar' sample with VB.net

I thought I'd share this as it took some time for me to work this one out.

I wanted to use the AJAX Control Toolkit HTML Editor, but wanted less functionality than the standard implementation. In fact, the Lite top toolbar without bottom toolbar was what I wanted.

To do this you need to copy the HTMLEditor.Samples.cs into your App_Code directory. You'll find it in Ajax Control Toolkit\SampleWebSite\App_Code\.

If, like me, you use VB rather than C#, then you'll need to convert it. I did this using the amazing tool at developerfusion.com. Obvisously, change the file extension to .vb too.

Then at the top of your aspx page you need to register it:
<%@ Register
TagPrefix="customEditors"
Namespace="AjaxControlToolkit.HTMLEditor.Samples" %>
I'm assuming here that you've already used the AjaxControlToolkit in your project, otherwise you'll have to register the main assembly too.

And finally you can add your control like so:
<customEditors:LiteNoBottom ID="myEditor" runat="server" Height="300px" Width="100%" TabIndex="2" NoScript="true" NoUnicode="true" />
Works for me!

Monday, 2 November 2009

Send javascript alert to client from codebehind

This is definitely really useful. I (mostly) love ASP.NET!
ScriptManager.RegisterClientScriptBlock(Me, GetType(Page), "clientAlert", "alert('This is the alert text');", True)
or in C#:
ScriptManager.RegisterClientScriptBlock(this, typeof(Page), "clientAlert", "alert('This is the alert text');", true)

Ajax autocomplete example

Auto completion of a text box is a great way to either suggest and/or restrict user input. After typing a specified number of characters an asynchronous call is made to the database to search for entries that match the input, and they are then displayed at the client in a selectable list beneath the input box.

The ajax control toolkit gives us a neat way of achieving this. I did however, for some reason (probably user error!) have trouble implementing the example they provide. I eventually got it all working and I thought I'd post it here for my reference.

Here's the web service code. In this case my service was called questionCategoryTags:

Public Class questionCategoryTags
Inherits System.Web.Services.WebService


Public Function GetCompletionList(ByVal prefixText As String, ByVal count As Integer) As String()

Dim stringArray() As String
Dim sds As New SqlDataSource
Dim ds As New DataView

sds.ConnectionString = ConfigurationManager.ConnectionStrings.Item("myConnectionString").ToString
sds.DataSourceMode = SqlDataSourceMode.DataSet

sds.SelectCommand = "SELECT DISTINCT Tag FROM Tags WHERE (Tag like '" & prefixText & "%')"
ds = CType(sds.Select(DataSourceSelectArguments.Empty()), DataView)

Dim stringList As New List(Of String)

For Each thing As DataRowView In ds
stringList.Add(thing.Row.Item("Tag"))
Next

stringArray = stringList.ToArray

Return stringArray


End Function


End Class

And don't forget to have this line at the top of your web service. It won't work without it:
system.web.script.services.scriptservice()

And here's the page source:

<ajaxcontrolkit:autocompleteextender completionsetcount="12" delimitercharacters=", " id="AutoCompleteExtender1" minimumprefixlength="2" runat="server" servicemethod="GetCompletionList" servicepath="questionCategoryTags.asmx" showonlycurrentwordincompletionlistitem="true" targetcontrolid="TextBox1" CompletionInterval="1000">
</ajaxcontrolkit:autocompleteextender>

Wednesday, 28 October 2009

Adding javascript or style sheet links to page head

You can do this dynamically for any page, even if it's header is provided by a master page elsewhere. Simply use the following code in your Page_Load event:

A link to a JS file. In this case it's Scriptaculous:
Dim jslink As HtmlGenericControl = New HtmlGenericControl("Script")
jslink.Attributes.Add("type", "text/javascript")
jslink.Attributes.Add("src", "/Scripts/scriptaculous.js")
Me.Header.Controls.Add(jslink)

A link to a CSS:
Dim link As HtmlLink = New HtmlLink()
link.Attributes.Add("type", "text/css")
link.Attributes.Add("rel", "stylesheet")
link.Attributes.Add("href", "../css/sortableLists.css")
Me.Header.Controls.Add(link)

Wednesday, 14 October 2009

Hide your datapager when there's only one page

It seems really strange to me that this isn't a configurable property of a datapager. If your datapager is in the LayoutTemplate of your Listview then this code will hide it for you:

Protected Sub ListView1_PreRender(ByVal sender As Object, ByVal e As System.EventArgs)
Dim pager As DataPager = DirectCast(ListView1.FindControl("DataPager1"), DataPager)
If pager.PageSize < pager.TotalRowCount Then
pager.Visible = True
Else
pager.Visible = False
End If
End Sub

Just place this as an OnPreRender handler for the list.

Tuesday, 13 October 2009

Using a like % @parameter % clause in a stored procedure

I wasted a few minutes scratching my head over this simple one, so it's worth me noting it down:

whatever like '%' + @parameter + '%'

It's not what I first did...whatever like '%@parameter%'.

Doh!

Wednesday, 23 September 2009

Public DNS servers

If you or your clients ever seem to be having DNS problems, then try switching DNS settings to ones provided by OpenDNS.

Their nameservers are 208.67.222.222 and 208.67.220.220

Thursday, 17 September 2009

Validation groups in my add/edit ListView

A schoolboy error this one - I created a ListView and made the InsertItemPosition the footer row, enabled editing, and everything was working great. Then I added some requiredfieldvalidators to my edit and insert fields. Then there was a problem, I couldn't make an edit without the validator triggering on the insert fields, despite the input fields being named differently and the validators assigned accordingly. And then I remembered the ValidationGroup attribute, which by default doesn't appear in your validator.

To link a validator to an action, simply assign the same ValidationGroup attribute to the button and the validator.

Thursday, 10 September 2009

Adding a foreign key - SQL Server

I confess to sometimes not using foreign keys when really I should be. Well now I have no excuse for forgetting how to implement them:

ALTER TABLE Orders 
ADD FOREIGN KEY (customerid) REFERENCES Customer;

Tuesday, 1 September 2009

Changing table owner in SQL 2000

Note to self, so next time I dont' have to Google it:
EXEC dbo.sp_changeobjectowner @objname = 'Test.TestTable' , @newowner = 'dbo'

Wednesday, 26 August 2009

OnRowCommand fires when GridView sorting

When sorting is enabled in a GridView, OnRowCommand is fired for the column headers prior to the sorting event. This seems odd to me as I don't see the header row as a proper row, but I guess that's the way it is. The upshot of this is that if you add a functional button to your rows you need to specify a specific CommandName for it, so that the handler for OnRowCommand knows not only what to do, but when to (and when not to) do it:

Sub onRowCommandActioner(ByVal sender As Object, ByVal e As GridViewCommandEventArgs)

If e.CommandName = "SpecialFunction" Then
'do your special stuff
End If

End Sub


And for this you'd add the following parameter to your button field:

CommandName="SpecialFunction"


This means that your special stuff will only be fired for your specific command and sorting will be fired as normal. I was today however scratching my head for a little while today as it still wouldn't sort for me. This was resolved by putting my GridView into an UpdatePanel.

Monday, 24 August 2009

Function keys on my laptop

Visual Studio developers use their function keys a lot, particularly when debugging, and it was becoming a pain for me having to hold down the blue Fn key on my Dell Inspiron when stepping through code. It turns out that the only way of locking this button on (I had hoped for a 'Fn Lock' setting) is by going into the BIOS on startup (F2) and changing it in there. It's pretty obvious once you're there, but I'm amazed this had to be a BIOS setting rather than something a little more accessible.

Having done this I can now step through code with one finger, but I now have to use the Fn key to access my wireless, battery and multimedia keys. No problemo.

Monday, 17 August 2009

Dell chat support - what a pleasure

My DVD drive disappeared from my Inspiron laptop today, just when I needed it. I tried restarting, uninstalling and re-installing the drivers from Dell etc., but my machine just wouldn't recognise it, with errors in the Device Manaager telling me that the drivers weren't installed properly.

This is when I was relieved I had bought a Dell, because within a few clicks I was chatting online to a Dell support specialist called Sarafaraz (in the Americas - not sure where, it could have been Bolivia for all I know) who logged into my machine using GoToAssist.

I sat back and watched the action - they tried exactly what I had done, and it didn't work. Then after a few registry edits and a remote restart it was all working again. Apparently a documented issue caused by multiple disc-burning software. Very odd, but Sarafaraz sent me a link with instructions to do the same fix if it happens again.

All in all a great experience; I didn't have to queue to wait for attention, and I could get on with some work on my other machine in between our exchanges. Brilliant!

Tuesday, 11 August 2009

Grammar, detail and OTT SEO

I was looking into online payment solutions (Payment Service Providers, don't you know) and ended up drifting a bit via the Protx (sorry, Sage Pay) website to have a cheeky look at their 'Web Developers' page. This is how I ended up on a digital agency's website to be confronted by the following grammatical clanger, on the home page, right at the top.
working with clients from small start-ups through to large corporate's has given us a great perspective


If I were a potential client I may be worried by this lack of attention to detail, but to be fair I've seen this kind of thing before. When a digital agency builds their own website often the same processes aren't followed as would be for client work, content is subject to less scrutiny, and the apostrophe-abusers have free reign.

And then I found another PSP, whose name you can see below. They also have a grammatical error (independant) but amazingly they feature an offer on their homepage that expired 9 months ago. Is anybody actually there?


I didn't stay long enough to find out.

And then, onto another well-known PSP, Netbanx. They've been in this business for a long time; I recall using them several years ago for an entertainment ecomms site that I built. But their website now is a big disappointment - it feels like it was written entirely by an SEO (Search Engine Optimisation) expert. It says lots of the same thing over and over again in lots of different ways, with lots of links to the same things said in a different way yet again, all in the pursuit of a good position on Google. And it actually manages to provide me with absolutely none of the information I need!

Monday, 3 August 2009

Creating a new app pool in IIS

This is one of those things I do so infrequently that I keep having to re-learn it every time. This is made more difficult by the fact that I find the IIS interface so completely unintuitive; I always end up going round the houses and then pronouncing "ah, that's it!" when I finally remember what I did last time.

The thing I always struggle with is the fact that the 'Application Pools' branch seen in the image below doesn't really do what I expect it to do.


I start by right-clicking on the Application Pools folder and choosing to create a new App Pool. then I call it whatever I want, and then it appears in the list of Application Pools. Great....so now what?

Well, the Eureka moment comes later in the 'Web Sites' folder. Select your web site, or a directory within it, right-click on properties, and select the 'Home Directory' tab. You'll see this:


At the bottom is an 'Application Settings' interface - it is here that you setup your web-site (or part of it) as an application (call it whatever you want), and it is in the last drop-down called 'Application pool' that you will the App Pool that you created earlier. Hey Presto.

For some final tweaks you'll want to set the Execute permissions to something more than 'None', and you'll want to go into the ASP.NET tab in the Properties window to set the version. Sorted.

Friday, 31 July 2009

Symantec's Norton Ghost - not the experience I had hoped for

I recently bought a new laptop (64bit Vista. Brave or stupid?) and it took me a very long time to install and configure everything I needed, simply because of the amount of tools I need. This prompted me to do something I've been meaning to do for ages: sort out a disk backup/recovery system so if ever it fails on me, or I drop it, or it gets nicked, I can restore it to pretty much the same state without losing a whole week of my life.

Now, I could have spent some time researching the options and I'm sure there are some OK open source solutions out there, but I figured for £40 I could save time, and I couldn't go wrong with Norton Ghost, from Symantec. A big name with a huge B2C paying customer base must have a slick, intuitive, bomb-proof solution, right? You'd have thought so.

Installation went well, but my first problem arose when the product first ran. Simultaneously I was launched into two different interfaces and processes - the easy startup interface and the Live Update tool, which checks for and installs product updates. This did indeed find a product update for my newly-installed software, and attempted to install it, but the following happened:



From this point onwards I couldn't run Norton Ghost without watching Windows desperately loop in circles telling me to wait 'while Windows configures Norton Ghost' and that the functionality I was requesting wasn't available in the file C://God Knows Where/Tmp/Completely Random Place/Blah blah blah/NGhost14.msi.

So I sought help from the website, downloaded and ran the removal tool as advised, and started again.

After installing again, I chose not to run Live Update this time as it gave me problems last time. This turned out to be a mistake, as several reasonably lengthy attempts to perform a backup resulted in the following non-descript problems. Interestingly, clicking on the 'more help on the web' link did absolutely nothing.





Only later through despair did I try running Live Update again. This time it worked and updated my newly installed version of Ghost. And, lo and behold, the next time I tried a backup, it worked.

So for my next question...how do I test the recovery process? Do I really want to practise a full disk recovery? Or, conversely, after my experiences so far do I really want to put my faith in it only to find out it doesn't work just when it really matters? hmmmmm.

I'll look into this, but assuming it works ok I think £40 for this little lot is a good deal.

Tuesday, 28 July 2009

Trimming text notes

It's been 20 days since my last post, in which time the Pitt family has grown by one and I've done very little programming, choosing sleep in my down-time rather than looking at a screen. But here's an issue I made a draft entry of a while back but never posted it for some reason.

I had some Revit files that were showing unexpected characters at the end of inserted text notes. I say unexpected, as I wasn't expecting them, so neither was our software. Closer inspection revealed these to be linefeeds or carriage returns, unprintable in your everyday text editors but nonetheless there when handling the data and making database comparisons. They got there because of the tendency to hit 'return' when completing a text note; something unpreventable, so I have to clean them up.

I looked into the .net String.Trim method, but this didn't seem to work. Then I had a look at String.Replace, to replace the offending character with nothing. The problem then was identifying what the character actually was. I tried \n, VbCrlf, etc etc to no avail.

Then after a coffee I tried Trim(String), which I had imagined did the same thing as String.Trim. This time it worked.

So what's the difference between String.Trim and Trim(String)?

Public Function Trim(ByVal str As String) As String
Member of Microsoft.VisualBasic.Strings
Returns a string containing a copy of a specified string with no leading spaces (LTrim), no trailing spaces (RTrim), or no leading or trailing spaces (Trim).

Public Function Trim() As String
Member of System.String
Removes all leading and trailing white-space characters from the current System.String object.

Subtle, and not entirely clear, but it works for me :)

Wednesday, 8 July 2009

Get googling for Revit API help

Fellow blogger and all-round clever bloke Rod Howarth has founded a Revit API search engine using Google's custom search feature.



My blog is one of five blogs which the engine currently searches, and no doubt this will grow. I'm honoured to be recognised as a useful and reliable source of information, and to be there right from the start.

Perhaps I should get insurance?

DoEvents() with Revit

Just a quickie as a follow-up to my last post. DoEvents isn't recommended for .NET, but given that Revit doesn't support multi-threading we have to use it sometimes to keep our UI responsive whilst we crunch some data.

It's commonly referred to as Application.DoEvents() but in your Revit plugin Application will probably refer to the Revit app and DoEvents won't be recognised as a method. So you need to use this:

System.Windows.Forms.Application.DoEvents()

Tuesday, 23 June 2009

Multi-threading with the Revit API

One of my Revit plugins has been using a BackgroundWorker to run a resource-heavy and lengthy operation. Running an operation on a separate, dedicated thread like this enables you to keep the UI responsive where otherwise it might appear as though it has stopped responding, or at best be sluggish.

Until recently this all worked fine, until I came across a problem in a very particular scenario. I wanted to loop through RVTLinks elements in a model using a category filter, and then get the boundingbox data for them. The model was opened using the API's OpenDocumentFile. This resulted in the following complaint from Revit:



I sent a test app to A'desk and they replicated the problem. Apparently this is happening because the Revit API "doesn't support multi-threading", and there is an SPR logged for this. This is strange for several reasons:

1. I've been using multi-threading with no problems for a long time.
2. The same code works fine if I have opened the file manually, rather than through the API.
3. Surely every Revit programmer needs multi-threading, as inherently a lot of the things we do are at some point resource-heavy? Or do I have to go back to the old DoEvents()?

Hopefully I'll get a bit more info on this soon.

Thursday, 18 June 2009

Revit 2010: Behavioural changes to dual-category elements

Dual-category elements? What are these?

Well, following on from an earlier post about curtain panel doors I sought some support from the API team at A'desk and I got a very thorough response which I thought was worth sharing as it might be of some interest, and would save them having to repeat themselves :)

"
In Revit, elements usually belong to a single category. But in two special cases, both related to curtain panels, it was possible for an element to belong to two categories. Let's call them the "normal category" and the "schedule category." Some parts of the software would use the normal category and other parts would use the schedule category, resulting in inconsistent behavior for such elements.

The first case is curtain panels that schedule as doors or windows. Such panels have a normal category of Curtain Panels and a schedule category of Doors or Windows.

The second case has to do with the ability to select a curtain panel and change its type to a wall type. Such a wall always has a normal category of Walls, and if its parameter "Categorize as:" is set to panel, then the wall will have a schedule category of Curtain Panels.

This project is to eliminate the dual-category nature of the above elements, and make them behave consistently as belonging to their schedule category. Thus, parts of the software that previously used the normal category will have different behavior now.


Behavior changes:
- Status prompt and selection filter: For dual category elements, Revit will show the schedule category instead of the normal category before.

- Marks: Dual category elements will warn when they share a mark with elements in their schedule category rather than their normal category. Unique type marks will be assigned to curtain panel door and window types.

- Visibility: For curtain panel doors and windows, they are hidden only when the schedule category (doors or windows) is hidden.

- Project browser: Curtain panel door and window types will be listed under Doors and Windows rather than Curtain Panels.

- Family category and parameters dialog: In the family editor, the dialog shows the category as Doors or Windows for curtain panel doors and windows, and all three categories (Curtain Panels, Doors and Windows) are listed in the list box, providing UI to change it.

- Family types dialog: For curtain panel doors and windows, Revit will show the parameters from Doors (Rough Width, Rough Height, Thickness, Fire Rating, Operation) or Windows (Rough Width, Rough Height, Operation) and but not those from Curtain Panels (Finish).

- ODBC export: Dual category elements will show in the tables corresponding to their schedule categories. The foreign key constraint between the Curtain Panels table and the Curtain Panel Types table will not be created.

- Graphic overrides (including pattern, halftone, transparent, and detail level): Curtain panel doors/windows will show the Doors/Windows overrides instead of the Curtain Panels overrides. Wall panels categorized as panels will show the Curtain Panels overrides instead of the Walls overrides.

- View filters: Curtain panel doors/windows will be hidden or overridden if they match the Doors/Windows filter criteria instead of the Curtain Panels filter criteria. Wall panels categorized as panels will be hidden or overridden if they match the Curtain Panels filter criteria instead of the Walls filter criteria.

- Tab order: Curtain panel doors/windows get a higher priority, and can be picked without tabbing. Wall panels categorized as panels get a lower priority, and can be picked using tabbing.
"

So now you know! :)

Wednesday, 10 June 2009

'Edit and Continue' in Revit 2010

In my last post I was disappointed to learn that Visual Studio 2008 (or 2010) doesn't support 'Edit and Continue' in 64-bit, so I was wondering how I might install Revit 32 on my 64-bit machine so I could make use of this valuable functionality.

Well, thanks to a comment from Matt Mason and an experiment here on another machine my disappointment is now even greater; It looks like my simultaneous switch to a 64 bit machine and Revit 2010 was actually disguising an even greater issue - you can't Edit and Continue in Revit 2010 at all!.

If you try it, you'll see this:



I tried this with the same code using Revit 2009 and 2010, on a standard 32-bit machine. Both times I had the same reference to the 2010 RevitAPI.dll. I can Edit and Continue in 2009. In 2010 I can't. If I need to change my code I have to stop debugging, make my edit, start debugging again, wait for Revit to start up again, select my add-on from the external tools menu, etc... How laborious.

Is this the way it's always going to be? Or am I doing something wrong? Argh.

See comments for update...

Monday, 8 June 2009

Debugging in 64 bit

Oh dear. Not long ago I took the plunge and bought a nice new laptop with 64-bit Vista, knowing that the world was slowly shifting towards the world of 64. I'm normally a late adopter of new technology, preferring to leave the hassle to others, and I'm now regretting breaking my own policy.

I have since discovered that a valuable piece of debugging functionality in Visual Studio does not work in 64-bit, and will not be fixed with the VS 2010 release either. I'm referring to "edit and continue" - the ability to alter your code while stepping through. This is invaluable when debugging.

And while we're on the subject, did you know you can't install Revit 32-bit on your 64-bit machine? Despite the fact that the ADN offers two download links for Revit (one for 32, one for 64) they are actually the same packages, and at 1.4GB that's a lot of wasted time and bandwidth in downloading. When you run setup.exe it detects your OS, and if you're running 64 bit then you only get offered 64 bit Revit. I tried to be clever and navigate directly to the 32 bit MSI, but those boffins at A'desk have locked them down - you can't run them directly.

Hey, MicroSoft - please please please give us 'edit and continue' in 64-bit!

Hey, Autodesk - please please please give us a way to install Revit 32 on a 64 bit machine!

Friday, 5 June 2009

The RvtMgdDbg tool

I was reminded recently when seeking support from the ADN what a great tool RvtMgdDbg is. Anyone programming with the Revit API should have this installed by default.

Read this post on Jeremy Tammik's blog for download links and a good summary by Mikako Harada on how to use it.

Wednesday, 3 June 2009

Doors in curtain walls: 2009 vs 2010

In Revit 2009 if you place a door in a curtain wall it gets categorized as a Curtain Panel, and to select the door you need to highlight and right-click the wall, choose 'Select Panels on Host', and then click the door. Rolling over the door reveals its classification (you can click the image to zoom in):



In Revit 2010 however things have changed. A door in a curtain wall is understandably categorized as a Door. And this image shows the same file as previous that was created in 2009 but opened in (and converted to) 2010:

Monday, 1 June 2009

Comparing folder contents using dictionaries

Coming from a web programming background I properly cut my commercial programming teeth using PHP, although this was after a dabble with Fortran when studying engineering, and then later Turbo Pascal when studying geology.

I haven't touched PHP for a while, but there's two things I really miss - not having to declare variables (some call me agile, others lazy), and the simple associative arrays. They don't exist in VB.NET, but the dictionary is a pretty cool alternative. Like an associative array a dictionary allows you to add any object as an item with a string as a key to access it, and edit members later if you wish.

A common use, especially amongst BIM & CAD programmers, would be for handling file information, as I had to do today. The following code takes a 'master' and 'client' directory path, looks at all the files in them both and stores their names and modified dates as key/value pairs in dictionaries. Then we compare the dictionaries using SequenceEqual, and if there's a difference we loop through the master's keys looking for the keys and values in the client dictionary. Any files that are missing or different are returned in a collection, for action elsewhere.


Private Function compareTwoFolders(ByVal masterDir As String, ByVal clientDir As String)

Dim filesNeedingUdating As New Collection(Of String)
Dim currentFileInfo As FileInfo

'get all master files
Dim masterFileCollection As ReadOnlyCollection(Of String) = _
My.Computer.FileSystem.GetFiles(masterDir, FileIO.SearchOption.SearchAllSubDirectories)

'get all client files
Dim clientFilesCollection As ReadOnlyCollection(Of String) = _
My.Computer.FileSystem.GetFiles(clientDir, FileIO.SearchOption.SearchAllSubDirectories)

'create dictionary of master file names and mod dates
Dim masterFiles As New Dictionary(Of String, String)

Dim i As Integer = 0
Do While (i < masterFileCollection.Count)
currentFileInfo = My.Computer.FileSystem.GetFileInfo(masterFileCollection(i))
masterFiles.Add(fixFilePath(masterFileCollection(i)), currentFileInfo.LastWriteTimeUtc)
i = i + 1
Loop

'create dictionary of client file names and mod dates
Dim clientFiles As New Dictionary(Of String, String)

Dim z As Integer = 0
Do While (z < clientFilesCollection.Count)
currentFileInfo = My.Computer.FileSystem.GetFileInfo(clientFilesCollection(z))
clientFiles.Add(fixFilePath(clientFilesCollection(z)), currentFileInfo.LastWriteTimeUtc)
z = z + 1
Loop



'check if dictionaries are equal
If Not masterFiles.SequenceEqual(clientFiles) Then
'there are differences in the sequences, so lets investigate
'loop through master dictionary finding what's different
For Each masterKey As String In masterFiles.Keys
If clientFiles.ContainsKey(masterKey) Then
If Not clientFiles(masterKey) = masterFiles(masterKey) Then
'different mod dates, add file to collection
filesNeedingUdating.Add(masterKey)
End If
Else
'no file found, add file to collection
filesNeedingUdating.Add(masterKey)
End If

Next

End If

Return filesNeedingUdating

End Function


fixFilePath, which I haven't included, is just a function that reduces the full file path (C:\\ etc) to the directory that your interested in, so that they can properly be compared. If you're comparing between two machines with the identical file structures you won't need this.

Tuesday, 12 May 2009

Fix your file associations

For ages I had a problem where I couldn't associate a .dwg file with anything other than AutoCAD 2006. I have 07, 08, 09 and 10 on my development machine, and every time I tried to use Windows Explorer's Tools > Folder Options > File Types to specifically point to one of these installations, it would always switch back to AutoCAD 2006. So, every time I double-clicked a dwg it would open in 06.

Enter Creative Element Power Tools. My colleague found this after having a similar problem with MicroStation XM and V8i. It's a neat box of tricks that's full of good ideas, one of them being the ability to easily re-associate your file types. Even better is the ability to easily add right-click options to a particular file type, enabling you to specify which version you want to open in. So now when I right-click on a dwg I can select any one of my Acad installations.

And there's way more than this. Take a look yourself as there's 'too many to list' :)

Wednesday, 6 May 2009

Preprocessor directives and conditional compilation

If you want different code to be compiled at build time under different conditions you can use preprocessor directives. Using the hash (#) symbol you can write code that the compiler responds to.

A common example of this is the need to use different code in 64 bit and 32 bit applications*.

#If PLATFORM = "x64" Then
'put your 64 bit specific code here
#else
'you know
#End If

In this example we're poking the project's platform setting at compile time and acting accordingly. If the platform is set to 64 bit, the first set of code will be compiled. If the platform is set to 32 bit then the second set of code will be compiled.

You can define your own conditional compiler constants by using the #Const directive (only private to the file in which it is declared), or in the Project Properties dialog (which then has a public scope to all files in the project).


*Of course in some applications it might be more suitable to identify the bitness of the OS at runtime like this:

Private Shared Function Is64Bit() As [Boolean]
    Return Marshal.Sizeof(GetType(IntPtr)) = 8
End Function

Friday, 1 May 2009

Using pre-build events with SQL CE deployment

In a recent post I referred to the four dlls (sqlceer35EN.dll, sqlcese35.dll, sqlceme35.dll, sqlceqp35.dll) you need to carry around with your app to run a private installation of SQL CE. Drag them onto your project in your Visual Studio's Solution Explorer, set them as 'Content', and set them to copy to your output directory. Hey presto, there you have it.

BUT these four dlls are actually different for the 32 and 64 bit environments, so when you build for each platform you have to make sure you have the right ones. To automate this I used Pre-build events to copy the right ones in prior to build. 



Go to your project properties window and click on the 'Build Events' button. Here you're able to drop in code that gets stuff done prior to or after build. The syntax can include any command that is valid at the command line or in a .bat file, and this includes conditional statements that allow you to query certain project settings.

I'll cut to the chase, here's the code:

if $(PlatformName) == x64 xcopy /Y "C:\MyReferences\SQLServerCE\x64\*" "$(ProjectDir)"
if $(PlatformName) == x86 xcopy /Y "C:\MyReferences\SQLServerCE\x86\*" "$(ProjectDir)"

Thursday, 23 April 2009

Windows forms application using command line arguments

This is one of those (many) little things I do infrequently enough to have to 're-learn' it every time as I just can't quite remember the syntax. But every time I google it I seem to find lots of console application examples like this:

Public Sub Main(ByVal args() As String)
'do stuff
End Sub

Well, for the (or rather my) record, here's how you do it in a winforms app:

Dim args() As String = Environment.GetCommandLineArgs()

Wednesday, 15 April 2009

Switching to SQL Compact for Revit 64 bit

I recently posted that with the advent of Revit 64 some of us might be faced with having to update old systems and code for compatibility. Specifically for me this meant having to leave behind the old but reasonably faithful OLE DB provider for Microsoft Jet (no 64-bit version was ever provided for this) and adopt something new.

Microsoft's recommended database solution for single-user desktop (and mobile) applications is the free to use and distribute SQL Server Compact Edition (CE). With an approximately 5 MB memory footprint and a less than 2 MB disk footprint, SQL Server Compact Edition can run concurrently with other applications, making it effectively invisible to the user of the application. The app I'm working with extracts data from a Revit model, stores it in a local database, and compares it with data in another local database. For this, and I'd imagine lots of Revit plugins, SQL Server CE is ideal.

Of course, if you were starting a new project, or your app was small, you might considering using SQL CE along with newer technology like LINQ, which means you don't have to get your hands dirty with SQL stuff. But in the Real World some of us have big projects and little time, and need to adapt the code we have rather than re-write.

These are some of the things I had to consider when adapting our applications, which have plenty of different coding techniques from the hands of different developers:


Converting MS Access (.mdb) files to SQL Server CE (.sdf)

I used Primeworks's Data Port Wizard. It's intuitive and easy to use, and it did the job without a hitch.

When this is done, to open and manage your sdf files you can use Microsoft SQL Server Management Studio (SSMS) 2008 (but not 2005). To get this just download the full free trial of SQL Server 2008 and in the install process just go for the management tools only.


Sorting out SQL syntax and data types

Some of your SQL queries that did work won't work any more. Here's an example:

Select count(*) from TableName where myfield='hello'

Needs to be smartened up a little:

Select count(*) from [TableName] where [myfield]='hello'

And you may be using data types that aren't recognised in SQL CE. 'text' is one of them, for which I chose to substitute 'nvarchar'. But I'm no database expert - there may have been better options.


Changing connection strings

Typically I was changing my connection strings from something like this:


Private MasterFileConnection As OleDb.OleDbConnection

Dim connstring = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=myDatabase.mdb;Jet OLEDB:Database Password=letmein;"

MasterFileConnection = New OleDbConnection(connstring)

to something like this:

Private MasterFileConnection As SqlCeConnection

MasterFileConnection = New SqlCeConnection

MasterFileConnection.ConnectionString = "Data Source=myDatabase.sdf;Persist Security Info=False;Password=letmein;"

MasterFileConnection.Open()


Handling data

In this example we were using an ADODB recordset to append new rows to a table:


Dim rsRecord As ADODB.Recordset
rsRecord = New ADODB.Recordset

rsRecord.Open(TableName, DB, ADODB.CursorTypeEnum.adOpenKeyset, ADODB.LockTypeEnum.adLockOptimistic, ADODB.CommandTypeEnum.adCmdTable)

rsRecord.AddNew()
rsRecord.Fields("Fieldname").Value = newValue
rsRecord.Update()

rsRecord.Close()

and it needed to be changed to this:

Dim rsRecord As SqlCeResultSet
Dim cmd As SqlCeCommand = DB.CreateCommand()

cmd.CommandText = "select * from [TableName]"
rsRecord = cmd.ExecuteResultSet(ResultSetOptions.Updatable Or ResultSetOptions.Scrollable)

newRow = rsRecord.CreateRecord

newRow.SetValue(rsRecord.GetOrdinal("Fieldname"), newValue)
rsRecord.Insert(newRow)

rsRecord.Close()

and elsewhere using an OLEDB datareader we had to move from this:

Private command As OleDbCommand
Private drawingOptions As OleDbDataReader

command = New OleDbCommand("SELECT * FROM Tablename", MasterFileConnection)

If command.Connection.State = 0 Then command.Connection.Open()
drawingOptions = command.ExecuteReader(CommandBehavior.CloseConnection)

Do While drawingOptions.Read

'do something
Loop

drawingOptions.Close()

to a SQL CE datareader, like this:

Private command As SqlCeCommand
Private drawingOptions As SqlCeDataReader

command = New SqlCeCommand("SELECT * FROM [Tablename]", MasterFileConnection)

If command.Connection.State = 0 Then command.Connection.Open()
drawingOptions = command.ExecuteReader()

Do While drawingOptions.Read

'do something
Loop

drawingOptions.Close()


Deployment

To do the coding shown above you already need to have referenced in the System.Data.SQLServerCE.dll to your project.

To get things to work on the end-user's machine you can either add SQL CE as a pre-requisite to your setup file, which will identify if a machine has it installed or not and send the user to download it from Microsoft accordingly.

Alternatively you can create a 'private installation', which by carrying all the necessary files (sqlceer35EN.dll, sqlcese35.dll, sqlceme35.dll, sqlceqp35.dll, System.Data.SqlServerCe.dll) in your package avoids the need for bootstrapping and a possibly contentious separate download. A lot neater.

Friday, 3 April 2009

Troubleshooting my Revit installation

In a recent post I described a problem I was having with my home desktop. Revit was failing to load and the following error would display:




Unfortunately it still does this, and having exhausted seemingly all other avenues with the helpful people at Autodesk I'm now preparing to wipe my machine and start again. There were however a few useful tips I picked up along the way that are worth remembering if you have problems with Revit.


Inspect your journal file

Your journal files (C:\Program Files\Revit SomethingOrOther 20xx\Journals\***.txt) amongst many other things may give you more of an idea of what might be going wrong. If you have a recurring but unpredictable error you could compare journal files to see under what circumstances it occurs, and thereby replicate it. Failing this, take the relevant information from the journal file and google it - you might not be the only one experiencing this. Unfortunately in my case the journal file wasn't much use:




The installer log file

If like me your problem happened as soon as you installed then you may want to look at the installer log file. This can be found in your %temp% folder ("Start" -> "Run." -> type "%temp%" -> "OK"), and will be called something like "Autodesk Revit Architecture 2010 - Preview Beta Install.log". Mine's 16MB of records from the installation process. If something went wrong with install, I assume you'd see it here.


Use Dependency Walker

Dependency Walker is a brilliant tool for all developers.

It is a free utility that scans any 32-bit or 64-bit Windows module (exe, dll, ocx, sys, etc.) and builds a hierarchical tree diagram of all dependent modules. For each module found, it lists all the functions that are exported by that module, and which of those functions are actually being called by other modules. Another view displays the minimum set of required files, along with detailed information about each file including a full path to the file, base address, version numbers, machine type, debug information, and more.

Dependency Walker is also very useful for troubleshooting system errors related to loading and executing modules. It detects many common application problems such as missing modules, invalid modules, import/export mismatches, circular dependency errors, mismatched machine types of modules, and module initialization failures.

Google "Dependency Walker" and you'll end up on the free download site.

The output from DW did help a little, and seemingly narrowed my problem down to the initialisation of one of the AutoCAD UI components. In response to this, I was advised to do this:


Uninstall all Autodesk products and remove Autodesk from path variable

This is quite extreme, but all else had failed so far so I was advised to uninstall all Autodesk products. When uninstalled, I edited the system path variable by going to System Properties > Advanced > Environment Variables:



The entry highlighted in the image above shows the path you need to edit. Remove the Autodesk entry - it should appear something like this: "C:\Program Files\Common Files\Autodesk Shared".

When done, reboot, and re-install.

This didn't work for me, but it might do for you. So after hours of tinkering, I'm none the wiser. Today I'm going to compare my install and DW logs with a known-working install to see if I can spot anything obvious. Failing that, I think it's time to re-install windows :(

UPDATE: May 7th 2009. In the last week Google analytics tells me I've had nearly 40 visits from users with Revitmfc problems, so I know I'm not alone. My problem is still unresolved, I bought a new machine instead as I couldn't face a Windows re-install! If anyone does resolve this please let me know. As far as Autodesk are concerned they think I'm unique. My stats prove otherwise...

Thursday, 26 March 2009

Parameters in nested families

I was asked if I knew how to access parameters in a nested family using the API, and was sent some sample files. It turned out be more of a Revit user issue than a programmer issue, IMO.

I had to edit the nested family, open up the 'Family Category and Parameters' dialog, and check 'Shared' in the 'Family Parameters' window:



Then I had to reload it into the host family, and reload the host family into my project.

Then using this code, the parameters could be accessed:

Public Shared Function ExtractAll()

Dim elementIterator As Autodesk.Revit.ElementIterator
elementIterator = revitApp.ActiveDocument.Elements


Try

While (elementIterator.MoveNext())
Dim currentElm As Autodesk.Revit.Element
currentElm = elementIterator.Current


WriteOutput("Id = " & currentElm.Id.Value)
WriteOutput("Name = " & currentElm.Name)
WriteOutput("Type = " & elementIterator.Current.GetType.Name)


'go get the parameters
ParameterCheckerNew(currentElm)


End While



Return True

Catch ex As Exception
Debug.Print(Err.Description)
Return False
End Try


End Function


Public Shared Function ParameterCheckerNew(ByVal elem As Autodesk.Revit.Element)
Dim params As ParameterSetIterator = elem.Parameters.GetEnumerator

While params.MoveNext
Dim currentParam As Parameter
currentParam = params.Current
WriteOutput(currentParam.Definition.Name)

Select Case currentParam.StorageType

Case StorageType.Double
WriteOutput(currentParam.AsDouble.ToString)

Case StorageType.Integer
WriteOutput(currentParam.AsInteger.ToString)

Case StorageType.String
WriteOutput(currentParam.AsString)

Case StorageType.ElementId
WriteOutput(currentParam.AsElementId.Value.ToString)

Case StorageType.None
' nothing
Case Else
' nothing
End Select

WriteOutput(currentParam.AsValueString)
End While

End Function




Note above I'm simply looping through every element in the file. This is lazy coding, and you could create any kind of filter you want here to see only the elements you need.

Interestingly I couldn't seem to access it using a selection iterator, as only the host family is recognised as selected, not the nested family within. There's probably a way of drilling down into a selected family and seeing what's nested in it, but I'll take a look at that some other time.

Tuesday, 17 March 2009

Modifying a user's Revit.ini file

Historically I've been a bit cautious about this, as it just feels wrong sneaking into another Program's installation directory and modifying any file, let alone one with a .ini extension. But leaving my users having to edit their ini manually ad infinitum wasn't an acceptable long-term solution.

It was to my pleasant surprise that the SDK contains a sample of C# code designed to modify the Revit.ini. And very simple it looks too. It makes use of the windows registry functions WritePrivateProfileString and GetPrivateProfileInt.

Here I've knocked up a VB.net version and added one feature I wouldn't feel safe without - a backup copy in case it all goes wrong! Make a console application that starts in Sub Main, with two vb modules called anything you like. Drop this code into one module:

Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.IO

Module General

    Public Sub Main(ByVal args() As String)
        Try
            ' path to Revit.ini
            Dim iniFilename As String = "C:\Path to Revit installation\Revit.ini"

            ' check exists
            If File.Exists(iniFilename) Then
                'make backup in case anything goes wrong!
                FileCopy(iniFilename, iniFilename & ".BAK")
            Else

                Console.WriteLine("File ""{0}"" not found.", iniFilename)
                Throw New ApplicationException("Ini file not found")
            End If

            Dim externalCommands As String = "ExternalCommands"
            Dim commandName As String = "This is my command name"
            Dim className As String = "class.name"
            Dim commandDescription As String = "Whatever you like"

            Dim revitIni As New IniFile(iniFilename)

            ' get count if exists.  else 0
            Dim ecCount As Integer = revitIni.GetIniValue(externalCommands, "ECCount", 0)

            ' increment count
            ecCount += 1
            Try
                ' get writing
                revitIni.WriteIniString(externalCommands, "ECCount", ecCount.ToString())
                revitIni.WriteIniString(externalCommands, "ECName" & ecCount.ToString(), commandName)
                revitIni.WriteIniString(externalCommands, "ECClassName" & ecCount.ToString(), className)
                revitIni.WriteIniString(externalCommands, "ECDescription" & ecCount.ToString(), commandDescription)

            Catch ex As Exception
                Console.WriteLine("Write failed")
            End Try

        Catch ex As Exception
            Console.WriteLine("Add External Command failed.")
        End Try

    End Sub

End Module

and drop this into the other:
Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.Runtime.InteropServices


Public Class IniFile
    'path of ini file
    Private iniPath As String

    ''' 
    ''' Gets or sets the path of ini file
    ''' 
    Public Property Path() As String
        Get
            Return iniPath
        End Get
        Set(ByVal value As String)
            iniPath = value
        End Set
    End Property

    <DllImport("kernel32")> _
    Private Shared Function WritePrivateProfileString(ByVal section As String, ByVal key As String, ByVal val As String, ByVal filePath As String) As Integer
    End Function


    <DllImport("kernel32")> _
    Private Shared Function GetPrivateProfileInt(ByVal section As String, ByVal key As String, ByVal def As Integer, ByVal filePath As String) As Integer
    End Function


    Public Sub New(ByVal iniFilePath As String)
        iniPath = iniFilePath
    End Sub


    Public Function WriteIniString(ByVal region As String, ByVal key As String, ByVal value As String) As Boolean
        Return If(WritePrivateProfileString(region, key, value, Me.iniPath) = 0, False, True)
    End Function


    Public Function GetIniValue(ByVal region As String, ByVal key As String, ByVal def As Integer) As Integer
        Return GetPrivateProfileInt(region, key, def, Me.iniPath)
    End Function
End Class

Start her up, and assuming the path to your Revit.ini file is correct it should work. I'll be looking to run this .exe in our installation process so we can banish manual ini editing forever. Yay!

Thursday, 12 March 2009

Images in your Revit 2010 ribbons

Further to my recent post about Ribbons, you can add a 32 x 32 pixel image to your panel button using this code:

myPushButton.LargeImage = New BitmapImage(New Uri("C:\File Path\myImage.bmp", UriKind.Absolute))

And you'll need this too:

Imports System.Windows.Media.Imaging

And here's what it looks like using a picture of a bit of my Lambretta:



If you use a bigger image then it doesn't seem to get resized, Revit just shows the top-left 32 x 32 square. What's particularly interesting is that you can call your images from the web, like this:

myPushButton.LargeImage = New BitmapImage(New Uri("http://www.mydomain.net/images/myImage.gif", UriKind.Absolute))

Notice it doesn't have to be a bitmap. You can use gif, png etc, and if you're a whizz with photoshop you can specify transparent colours. I did try an animated gif and to my relief it didn't work. Imagine having a 'spinny logo' vying for your attention while you're trying to do some work!

The little things

Rod recently highlighted one of the smaller and very welcome improvements in Revit 2010 - user feedback when Revit fails to load an External App or Command. Previously there was an ominous silence and the user was left somewhat in the dark.

Further to this, Revit no longer silently renumbers the Revit.ini file entries when they fail to load or start.

This was such a pain, good riddance I say!

Tuesday, 10 March 2009

Ribbons in Revit 2010

Revit 2010 due to be released next month sees the introduction of ribbons to the interface, as first popularised in Microsoft Office 2007, then adopted by Autodesk for AutoCAD 2009.

In a recent post I put up a bit of code showing a simple menu and toolbar for Revit 2009, but finally I've overcome some of my installation problems and had chance to have a play with said ribbons in the forthcoming release.

All add-ins appear under the top-level 'Add-in' tab. External tools appear under the push-button menu 'External Tools' in the 'External' panel, and you can add panels to this ribbon like so:



And here's the code to do this:


Public Class RibbonSample
   Implements IExternalApplication

   Public Function OnStartup(ByVal application As Autodesk.Revit.ControlledApplication) As  _
   Autodesk.Revit.IExternalApplication.Result Implements Autodesk.Revit.IExternalApplication.OnStartup

       Try
           'push button
           Dim ribbonPanelButtons As RibbonPanel = application.CreateRibbonPanel("Panel name")
           Dim pushButtonOpenWD As PushButton = ribbonPanelButtons.AddPushButton("Tooltip Bold Title", "Button Name", "C:\pathToMy\coolfunction.dll", "className")
            pushButtonOpenWD.ToolTip = "Tooltip description"

           Return IExternalApplication.Result.Succeeded
       Catch ex As Exception
           MessageBox.Show("Ribbons Failed")
           Return IExternalApplication.Result.Failed
       End Try
   End Function


  Public Function OnShutdown(ByVal application As Autodesk.Revit.ControlledApplication) As  _
  Autodesk.Revit.IExternalApplication.Result Implements Autodesk.Revit.IExternalApplication.OnShutdown

       Return Autodesk.Revit.IExternalApplication.Result.Succeeded

   End Function


End Class

When (or if!) I find the time I'll have a go at adding bitmap images and drop-down menus to my buttons.

Thursday, 26 February 2009

Virtual Desktops

I had a pint with a friend last week who keeps extolling the virtues of the Macintosh. One of his big plus points was the multiple desktops offered by the Unix OS.

Multiple desktops are indeed great for the busy multi-tasking programmer, but using a Mac clearly isn't an option for us Revit lot. So I dabbled with a couple of  'virtual desktop' apps for my XP machine and have found VirtuaWin to be pretty cool.

Now I can separate comms, development, support etc. A lot neater!

Monday, 23 February 2009

Looping through selected elements

Fred emailed and asked about how to iterate through a selection set and make a modification to each element in that set, in particular regarding adding a leader to selected text notes.

The API provides the ElementSetIterator class. I used its method MoveNext in a While loop, and this takes us through each selected element, on which you can then test for type and act accordingly.


Public Sub LoopSelectedElements()

Dim activeDoc As Document = revitApp.ActiveDocument

Dim selectionIterator As Autodesk.Revit.ElementSetIterator
selectionIterator = revitApp.ActiveDocument.Selection.Elements.ForwardIterator

'loop through iterator
While selectionIterator.MoveNext

Dim currElement As Autodesk.Revit.Element = selectionIterator.Current

'see what elements we've found
Debug.Print(currElement.Name.ToString)

'check for type and make mods
If TypeOf currElement Is Autodesk.Revit.Elements.TextNote Then
Dim textNote As Autodesk.Revit.Elements.TextNote = currElement
textNote.AddLeader(Enums.TextNoteLeaderTypes.TNLT_STRAIGHT_R)
End If

End While

End Sub

I didn't really test the textNote.AddLeader part. Fred reports that it works but the new leader only becomes visible when you move the textnote. Something to look into when I've got a spare five mins :)

Friday, 20 February 2009

Simple menu and toolbar in VB

The focus on C# in the Revit SDK code samples can be a hindrance to those of us who prefer VB. Yesterday I had a request from a reader (or is it a viewer? subscriber? follower? what do you call someone that visits your blog?) asking for a sample of VB code that creates a simple menu and toolbar. Well, here's some that should do the trick:


Imports System
Imports System.Collections.Generic
Imports System.Text

Imports Autodesk.Revit


Public Class EdsToolbar
Implements Autodesk.Revit.IExternalApplication


Public Function OnShutdown(ByVal application As Autodesk.Revit.ControlledApplication) As Autodesk.Revit.IExternalApplication.Result Implements Autodesk.Revit.IExternalApplication.OnShutdown

Return Autodesk.Revit.IExternalApplication.Result.Succeeded

End Function


Public Function OnStartup(ByVal application As Autodesk.Revit.ControlledApplication) As _
Autodesk.Revit.IExternalApplication.Result Implements Autodesk.Revit.IExternalApplication.OnStartup

Try



'custom tool bar with buttons
Dim toolBar As Autodesk.Revit.Toolbar = application.CreateToolbar()
toolBar.Name = "Ed's Tools"

'image for toolbar, set to nothing by default
Dim imagePath As String = ""
toolBar.Image = imagePath

'menu
Dim menuItem As MenuItem = application.CreateTopMenu("Ed's Tools")
Dim menuItem1 As MenuItem = menuItem.Append(menuItem.MenuType.BasicMenu, "Open Working Directory", "C:\Program Files\Revit Architecture 2009\Program\EdPittOpenWrkDir.dll", "EdPittOpenWrkDir.OpenWD")

'toolbar
Dim item As ToolbarItem = toolBar.AddItem("C:\Program Files\Revit Architecture 2009\Program\OpenWorkingDirectory.dll", "OpenWorkingDirectory.OpenWorkingDirectory")
item.ItemType = ToolbarItem.ToolbarItemType.BtnRText
item.StatusbarTip = "Open Working Directory"
item.ToolTip = "Open Working Directory"
item.ItemText = "Open Working Directory"


Catch ex As Exception
MsgBox("Failed")
Return IExternalApplication.Result.Failed
End Try

Return IExternalApplication.Result.Succeeded

End Function

End Class

Create a dll from this, and then in your .ini file drop in the following:

[ExternalApplications]
EACount=1
EAClassName1=EdsTools.EdsToolbar
EAAssembly1=C:\Program Files\Revit Architecture 2009\Program\EdsTools.dll

Obviously if you already have some External Applications you need to modify this to suit.

Monday, 16 February 2009

Creating a new drafting view

I saw a query on the discussion groups about creating a draft view and setting the title on sheet, so I thought I'd have a stab at it and make a little post-ette on t'blog.

Dim newView As ViewDrafting = revitApp.ActiveDocument.Create.NewViewDrafting

newView.ViewName = viewName
newView.Scale = viewScale
newView.Parameter("Title on Sheet").Set(sheetName)


And of course if you want to create other types of view these are the available methods:

Document.NewView3D - Creates a new 3D view.
Document.NewViewDrafting - Creates a new drafting view.
Document.NewViewPlan - Creates a plan view based on the specified level.
Document.NewViewSection - Creates a new section view.
Document.NewViewSheet - Creates a new sheet view.

Thursday, 5 February 2009

Calculating building height - again

My last post was an attempt to calculate building height, but I quickly realised after posting (and luckily before being rumbled by some smart alec) that what I was doing was calculating the height at which the uppermost level was placed. Any thing on the uppermost level was being ignored.

So, here's another idea, and it assumes that your building has a roof at the top, which I'm sure in most cases is a fair assumption(?). It uses a category filter to look for roofs and then it queries the Max Z value of the BoundingBox of the roof:

Dim element As Autodesk.Revit.Element
Dim zValue As String

'active view used later for boundingbox
Dim activeView = revitApp.ActiveDocument.ActiveView

'get all elements in the Roof category
Dim CatFilter As Autodesk.Revit.Filter
CatFilter = revitApp.Create.Filter.NewCategoryFilter(Autodesk.Revit.BuiltInCategory.OST_Roofs)


Dim result As New List(Of Autodesk.Revit.Element)
Dim NumLevels As String = revitApp.ActiveDocument.Elements(CatFilter, result)

       
For Each element In result

'change this to footprintRoof etc if you wish            
If TypeOf element Is Elements.ExtrusionRoof Then


If element.BoundingBox(activeView).Max.Z > zValue Then
    zValue = element.BoundingBox(activeView).Max.Z
End If
   
WriteOutput(element.BoundingBox(activeView).Max.Z)

End If

Next

You could of course remove the 'If TypeOf' conditional and replace it with a try catch which would be a cheeky way of looking for the Max.Z in every element that the roof category filter returns, catching those that don't support it, and returning values for those that do. This would then return the Max Z value for all types of roof in your model.

In fact that's given me an idea. How about this, this will scan through every element in your model and return the largest Z value it can find. Is this the height of your building? :

Dim elementIterator As Autodesk.Revit.ElementIterator
elementIterator = revitApp.ActiveDocument.Elements

Dim topElement As Autodesk.Revit.Element
Dim element As Autodesk.Revit.Element
      
Dim zValue As Double
zValue = 0
Dim activeView = revitApp.ActiveDocument.ActiveView

While (elementIterator.MoveNext())

 element = elementIterator.Current

 Try
  If element.BoundingBox(activeView).Max.Z > zValue Then
   zValue = element.BoundingBox(activeView).Max.Z
   topElement = element
  End If
 Catch ex As Exception
  Debug.Print(Err.Description)
 End Try

End While

WriteOutput("The highest Z value is: " & zValue)
WriteOutput("The element is: " & topElement.Name)
WriteOutput("The element ID is: " & topElement.Id.Value)

The answer is no, I just tried this last idea on the Revit training sample c_Condo_Complex.rvt and of course it takes a long time to run, but it gave me the value for the 3D view. If you look for all elements, they won't all be part of your building!

So stick with the first idea above. I bet there'll be a comment in soon telling me there's a really easy way to do this, like building.height :)

Wednesday, 4 February 2009

Calculating building height

I saw a query on the Revit API discussion groups about how to calculate the height of a building, so I thought I'd have a quick go. Admittedley they wanted it in C#, but I like my VB.

Loop through your elements looking for a level type, and when you find one use the level.ProjectElevation property to get its height - in decimal feet of course. At first I added these up to get total height, but then realised that ProjectElevation returns the elevation of the level relative to the project origin, so all we want is the highest figure we can find of all our levels.

Dim elementIterator As Autodesk.Revit.ElementIterator
elementIterator = revitApp.ActiveDocument.Elements
     
Dim height As Double

While (elementIterator.MoveNext())

'look for level types
If TypeOf elementIterator.Current Is Autodesk.Revit.Elements.Level Then

Dim level As Elements.Level = elementIterator.Current

'is current value higher than previous values?
If level.ProjectElevation > height Then
    height = level.ProjectElevation
End If


End If

End While

MessageBox.Show("Height: " & height)
      

UPDATE: This only retrieves the elevation of the uppermost level, which isn't the height of the building - see my next post for a different approach!

Wednesday, 28 January 2009

instance.name and families with no types

I was using instance.name to establish whether users had placed a particular detail component or not, but discovered I'd made an assumption that that turned out to be incorrect, because I based my coding conditions on research with typeless families only.

Take the 'Dbl Tri Wall tie -Section' for instance (Metric Library\Detail Components\F - Masonry\F30). On inspection, this is a family with no types. Click 'Family Types' in the Family Editor and you'll see in the dialog that 'Name' is greyed out unless you add a type.



Compare this with  'C Joist - Section' (Imperial Library\Detail Components\Div 05-Metals\054200-Cold-Formed Metal Joist Framing), and you'll see there's lots of types, all named by their size, and all with the same parameters with different values.



When you use these elements in your model, and look at the element properties, you'll see that the family with types (the C Joist) populates the 'Type' field with the type name:



But the family with no types (the wall tie) just uses the family name again:



It is this that is returned by instance.name. When my code came across an instance of an element with types, and was expecting to see the family name, it didn't, and made the wrong decision. A few mods later and it was fine.

Note to self: do more research before making assumptions.



Tuesday, 20 January 2009

Checking your displayed units

To find out what units your document is displaying you can use this:

Document.DisplayUnitSystem

This will return zero for metric, and 1 for imperial. It would be handy (for me at least) for this to be changeable through the API, but unfortunately its a readonly property.

I had a bit of a play with my unit settings (Settings > Project Units) to clarify how this property behaves, and confirmed that if I make Area and Volume imperial but leave Length metric, then we still have a metric '0' setting. This is what I was expecting, but I had to make sure!

Monday, 5 January 2009

Revit failing to load - 0xc06d007e

I'm posting this to help anyone who might have the same rather strange problem. On my machine at home Revit would fail to load if I tried to add any External Command. All my syntax and naming was correct, all files were present and correct, but Revit would still silently fall over when booting up, without an error message. If I changed the ECCount back to zero then it would boot up fine.

Looking at the journal files (C:\Program Files\Revit Architecture 2009\Journals) for the load failure revealed the following:

Exception occurred
ExceptionCode=0xc06d007e ExceptionFlags=0x00000000 ExceptionAddress=7C812AEB


I sought help from Autodesk but they were stumped too, and a long support ticket ended in an internal change request being submitted which I thought was a little odd as if they fail to diagnose a problem how do they know what to change?

I was left with my home machine unable to run any EC's, and this was the status for several weeks until I looked into the issue again. I discovered that on another machine that was working fine if I removed RevitAPI.dll from the Program folder then started Revit I could replicate the same behaviour - silent load failure and exactly the same entry in the journal file.

So I went back to my home machine and renamed RevitAPI.dll to RevitAPI.old (the same as removing it), started Revit (it failed as expected), then renamed it back again to its proper name, started Revit again, and now it works fine.

I have no idea why this works or what was wrong in the first place. But if you have the same problem hopefully your google search will point you this way and your problem will take minutes to solve rather than weeks!