Data Design Time Changes Beta1 to Beta2

Data Design Time Changes Beta1 to Beta2

Rate This
  • Comments 44

These are some of the feature changes we’ve made within the Data Design Time features.  When I started writing this document I thought about making it a bulleted list, but then I realized this isn’t a Whidbey Feature list, it’s a delta from Beta1 to Beta 2.  Those interested in the delta probably really want to know what went into the decision.  If you’re looking for a Whidbey Feature list, this isn’t it. When we get closer to Whidbey RTM I’m sure we’ll get something like that pulled together.



Q:         What features does your team own?

A:        My team sits in the middle of a couple of stacks of teams.  We own:

·         The Typed DataSet Designer and Typed DataSet code gen

·         The Data Wizards such as the DataAdapter wizard. 

·         The Data Sources Window

·         Most of Server Explorer

·         Much of the Local Data experience. 

While our focus is primarily on data design time for client apps, including Windows Forms, Devices, Visual Studio Tools for Office and SQL Reporting, we do provide the Data Set designer experience for Web Projects as well.  However, the Venus team owns the overall data design time experience when building web forms.


Q:         Are their differences between VB and C# features?

A:        There are some situations where we couldn’t provide 100% parity between the languages.  For instance VB can wire events using the Handles clause.  This simplifies our code gen as we don’t need to modify a constructor or InitializeComponent to enable the event handling.  This has an effect in the Typed DataSet partial classes for handling DataTable events.  Due to the infrastructure of the SingleFileGenerator, we weren’t able to provide the same experience for C#.  However, in C# you can override the OnColumnChanging method which essentially accomplishes the same task.  Another example is C++ doesn’t support Partial classes, so some features are just disabled.  In general, we’ve tried to provide the same features, but the experience may vary depending on the language feature set.


Q:         Will beta 2 reflect the final product, with just some bug fixes?

A:        For the most part, yes.  There are some minor “features” we still need to complete in our RTM milestone.  However their really isn’t any new feature work.  Just some fit and finish work.


Q:         Where do these feature requests come from?

A:        You, our customers.  Some of the changes are just a continuation of the work we started at the beginning of the product cycle.  Many are based on feedback from usability studies, newsgroups, LadyBug, feedback from attendees at conferences, etc. 


Q:         Can I ask for more?

A:        You can always ask, J.  However, the biggest request we hear is “ship already”.  We’re really trying hard to stabilize the product which means fix bugs, don’t add features.


Q:         What goes into your consideration to make a change?

A:        How many customers would the change affect?  Does it make something easier, or enable a new scenario?  If we changed/added it later would it break compatibility with something we would ship today?  What’s the cost/benefit of making the change? 


Q:         Are these all the changes you’ve made? 

A:        No.  These are a list of the things I think are most interesting.  I didn’t list the bugs we’ve fixed, just the feature work.  Just as a reference, as of January 7th 2005 our Data team alone has resolved over 2,100 bugs just for Beta 2.  The entire VB Product Unit has resolved over 10,500 bugs, and the VS Whidbey team has resolved almost 118,000 bugs just in Beta 2.  I also need to give a lot of credit to our partner teams such as the Client team, VDT, Venus, Devices, VSCore and DataWorks.  Working at Microsoft is a team approach.  There are a lot of people to be leveraged here.


Form Generation

Smart Captions

One of the suggestions from an MVP summit was to put spaces in the labels.  You ask for it, you’ve got it.  I like to say we reward developers for good programming practices.  If you follow “good” naming conventions, such as “FirstName” or even “First_Name”, we’ll create a label of “First Name:”  To accomplish this we use a Regex Expression.  We didn’t have time to expose this as a Tools-Options, but here’s the expression for reference.

Regex.Replace(name, "(\p{Ll})(\p{Lu})|_+", "$1 $2") + ":"


Minimize the noise in Me./this.

In almost all cases the labels of your controls are just labels, and never need access through code.  Yet when you type Me. or This. you see all these labels as well as your data bound controls.  One of the new features the client team added was a property called GenerateMember.  When set to False, the control is not generated as a class scoped member variable.  The control is created within InitializeComponent, added to the Forms collection, then falls out of scope as a variable name.  Since it’s still within the forms collection, the control still exists.  In the rare cases where you do need to interact with the Label the Data Sources Window created, simple select the label, and then toggle the GenerateMember property to True in the property grid.


Smart Defaults

We’ve done a lot of work to start with Smart Defaults.  We may not always generate the same names, or done the same work you might have done, but the way we look at it, it’s gotta be better then TextBox1.  You’ll see this in the Control Names, Labels, and even the code gen we do. 


The DataConnector has been renamed to BindingSource.  In usability studies we watched developers really struggle with the name.  They were absolutely convinced the DataConnector was a Database Connection object.  It didn’t help that that all the other Data Components their used to seeing on the ToolBox were missing, so they just assumed this must be their old connection object.  See my Blog for the thought process behind removing the raw data components from the toolbox.  To avoid this confusion we’ve named it more appropriately to BindingSource.  We’ve also renamed DataNavigator to BindingNavigator.  For the most part, these are the same controls as Beta1 with a rename and a lot of bug fixes.


One of the frequent requests, and one of my own pet peeves was configuring lookup controls such as a ComboBox.  This was a feature that got cut in B1 due to the schedule, but we were able to squeeze most of it back in.  There are two features that were added.  First, was a Smart Tag on Lookups like the ComboBox.  Within the Smart Tag you can set all the normal properties on a combob box.  The tasks even have a ToolTip that describes their purpose.


The second was Connect the Dots.  If you drag a “List” from the Data Sources Window onto a ComboBox VS will set the DataSource, DisplayMember and ValueMember to the elements of the list.  VS will create a BindingSource for the list and set it as the DataSource.  For DataSets, VS will take the first column in the Primary Key and set it as the ValueMember.  If you don’t have a primary key, or the list is a list of objects, VS will just take the first property it finds in the list.  VS will then look for the first string value in the column/list of objects and set it as the DisplayMember.  Remember, I said we use Smart Defaults.  These may not be the right values for you, but they at least get you started.

Save Code Gen

In Beta 1 we took the easy out.  We provided a Save button on the DataNavigator, but we disabled it, and left it to you to figure out the proper code.  We did this because we can really only generate the proper save for a single table.  We haven’t been able to finish the work to provide a “one liner” to save a master/details relationship.  After watching everyone struggle to figure out what the “right” code is, and then most of them forget to call EndEdit, we realized we needed to provide some starter code. 

In Beta 2 we now provide you the basic code gen.  “Smart Defaults” to get you started.

If Me.Validate() Then




    MessageBox.Show(Me, "Validation errors occurred.", _

        "Save", _

        System.Windows.Forms.MessageBoxButtons.OK, _


End If

If you’re saving multiple tables, or a master details relationship, you’ll need to add the additional save logic to parse the inserted rows from top->down and deleted rows from bottoms->up.  But it’s a starting point.

Databinding support for DBNull/Nullable(Of T)

To put it nicely, VS2003 data binding wouldn’t handle receiving DBNull very gracefully. While we weren’t able to update all the controls to support Null, at least the data binding infrastructure will support it.  This opens the doors for you and component vendors to support DBNull or Nullable(Of T) on controls such as a DateTime control.  Data binding also supports a NullMapping property.  So, when you do get a null value you can display a replacement value.  On the return trip it will be set back to Null, so be careful what value you choose.  If the default value is a valid value, then it will be converted to Null when pushed to the underlying data bound item.

Configure Master Details

As our feature set has evolved to include Object binding, it became obvious that it was confusing that you could create relationships with objects via the Data Sources Window, but had to use the Configure Master Details smart tag for DataSets.  Yet, the Configure Master Details smart tag didn’t work for objects.  I also believe that wizards are helpful, but they shouldn’t be a crutch and should be used in moderation.  The Data Sources Window will now display relationships under each table. We can now provide a consistent experience for DataSets as well as Objects.  Rather then having to drag the parent on the form then step through a smart tag you can just drag both the Parent and Child objects from the Data Sources Window.  If you drag from the root, you’ll get unrelated DataTable references.  If you drag a table from below another, you’ll get the relationship.  We’ve also cleaned up what we show as the relationship. Rather then showing the usually goofy relationship name FK_Orders_Customers, we will display the related table name.  If the parent has two relations to the same child table we’ll show the relationship name in parenthesis.  With this enhancement, the Configure Master Details smart tag and menu item were no longer needed and have been removed. 

Typed DataSet

Typed DataSet Code Gen goes on a diet.

Typed Column Events

I should start by saying our team loves type safety.  I still live by the motto that code in quotes is a bad thing and should be avoided at all costs.  While we’re working on shipping better tools for code coverage, this shouldn’t be a requirement just to catch simple typo mistakes.  Compile time verification is like checking your airlines website before heading to the airport.  No guarantee the plane will be on time even if the website says so, but if the website says it’s late…

As we had more time to work with these typed events we found many scenarios were incomplete yet we were generating a lot of extra code.  To have a full validation class you really need to compare the proposed values, not just the current values and these properties weren’t available on the EventArgs.  We needed more info at the row level as well.  We generated these events regardless of whether you used them or not.  What we wound up with was a ton of extra code that bloated the data set class for occasional, but incomplete value.  And, we didn’t have all the information available in a typed manner anyway.  Because of these issues, we felt it better to trim back, still support the validation scenario through partial classes, but not default code gen a bunch of extra stuff that you didn’t always use.  In Beta 2, when you double click a column in the DataSet designer, you’ll still get an event handler for the untyped column.  In VB, the default code gen will also demonstrate how you can reference columns in a type safe way so you wind up with a similar experience for a lot less cost. 

For C# we aren’t able to provide the exact experience due to way C# handles event wiring.  However, rather then just cut off C#, we enable it by leveraging the OnColumnChanging overload.

Private Sub EmployeesDataTable_ColumnChanging(ByVal sender As System.Object, ByVal e As System.Data.DataColumnChangeEventArgs) Handles Me.ColumnChanging

    If e.Column.ColumnName = HireDateColumn.ColumnName Then

' e.Row.SetColumnError(e.Column, "Error Info"

    End If

End Sub

By using the e.Column.ColumnName  for the comparison we’re still type safe.  If the developer changes the name of the HireDate column you’ll get a build error which the developer can simply double click in the error list. 

So, why don’t you generate a Select Case?

Select Case e.Column.ColumnName

    Case HireDateColumn.ColumnName

End Select

The lame answer is not all languages support Switch constructs. So CodeDom, which is what we use to generate the code in the Typed DataSet, doesn’t provide the ability for us to generate this code.

C# now gets Partial Class support for DataSets

In B1 the developer had to manually add the partial class for the DataSet.  In B2, we’ll now generate the partial class with the same entry points as VB.  Double clicking the designer, a DataTable, or selecting View Code from Solution Explorer.

DataSet EnforceConstraints

In many situations developers need to partially fill a DataSet.  Meaning although the DataSet contains several related tables, they only need to fill a subset set at a given time.  Let’s say you have a DataSet with Orders, Customers, OrderDetails and Products.  On one of the forms you only need Orders and OrderDetails.  You don’t need the associated customers loaded.  With DataSet.EnforceConstraints turned on as well as the relationship constraints turned on you would get an exception when attempting to load the Orders table as the CustomerId didn’t exist in the Customers DataTable within the DataSet.  Even when you do plan to load Customers you would need to load the tables in the proper order.  This was a very frustrating experience for developers.

Relationship constraints were turned on to enable the cascading updates and deletes.  This enables the user to delete Orders and all associated OrderDetail records are also marked as deleted.  The down side of this is the Data Tables wouldn’t enforce other constraints such as AllowDBNull or MaxLength. 

To find a better balance we’ve tweaked this a bit.  We now set DataSet.EnforceConstraints = true which was the same default in VS2003, but set the relationship constraints to false.  In VS2003, we didn’t create relationships by default, so this isn’t really a change compared to VS2003.  This means you now get column level constraints including MaxLength, AllowDBNull, but by default, will loose the cascading updates and deletes.  This can be changed by the developer easily within the DataSet designer relationship editor.  The best solution would be to decouple the Relationship Constraint from the Cascading features, but we just didn’t’ have the time to do this for Whidbey. 

DataTable Columns are now public

In 1.0 and 1.1 the typed DataTable columns were exposed as protected or friend.  This made it difficult to leverage them for type safe code.  If you wanted to reference information about a DataColumn in another assembly you were unable to get this info without cracking open the DataSet generated code replacing Friend to Public.  Rather then having to put code in quotes, you can now reference a column name using the following code:


Additional Info in the StrongTyping Exception

Let me first stat by saying I wish we had the time to expose Nullable(Of T) on the strongly typed columns.  This wasn’t as simple as you might think because DataBinding doesn’t actually use the Strongly Typed columns.  They use the underlying DataView L.

However, with that said, it’s very frustrating when you simply read a value from a strongly typed column and you get a StrongTypingException without any information as to what column threw it.  In B2 we did a simple but very helpful tweak by adding the offending column name to the text of the exception.

Throw New StrongTypingException("StrongTyping_CannotAccessDBNull for column Title", e)

SQL Server 2005 UDT Support

We had hoped to be able to include SQL Server UDT’s in the Typed DataSet as well as the TableAdapters however we just didn’t have the time to complete this work.  The Typed DataSet and TableAdapters will not be able to consume or represent a CLR UDT.  This doesn’t mean that you can’t consume Managed Sprocs.  It just means the schema and parameters must be SQL intrinsic types: (Char, VarChar, Bool, …)

This was a tough decision and one we thought a lot about.  Feedback from our close partners tell us that it’s unfortunate, but they can wait ‘till Orcas ‘till this feature comes online. Using CLR UDT’s has several complexities, including the requirement for the UDT to be on the server as well as the client. It was felt that CLR UDT’s weren’t as high a priority as managed sprocs, which we can consume. 

DataSet Skip Schema

When DataSets are serialized across Web Services, as well as the new Binary Format, we include the schema of the dataset as well as the payload.  For DataSets with only a few rows, the schema winds up being a large amount of the payload sent across the wire.  Since the DataSet is Typed, and the schema is already known on the consuming side, the Schema adds no value, but has a proportionally high cost.  In Whidbey we’ll now support a new property, SchemaSerializationMode, which can be set to ExcludeSchema.  This will only work with Typed DataSets when consumed by a Whidbey client, so the default will be the RTM behavior.


TableAdapters now placed in their own sub Namespace

In Beta 1 all table adapters were placed in the same namespace as the dataset.  The result of this meant that if you have two Typed DataSets with a table of the same name you would get a collision.  We had several choices.  One of them was to create the TableAdapters as nested classes within their associated DataSet class.  However, this imposed security problems as one of the goals was to limit the TableAdapters from being visible outside the assembly.  If the TableAdapter is a nested class, a developer could get at the code even though it was marked as friend/internal.  The next best solution we came up with was to place the TableAdapters under a namespace similar to the DataSet.  For instance all the TableAdapters for the NorthwindOrdersDataSet will be placed in:


If the Customer Table is defined in another dataset named NorthwindCustomersDataSet, the associated TableAdapter would be generated in:


The names are a little long, but if you’re using VS, intellisense comes to the rescue. 

TableAdapters go on a Diet

We had a lot of great ideas for TableAdapters, but now it’s time to ship and we need to trim back to the solid meat.

We originally hoped to enable TableAdapters for direct usage within Web Services.  For this reason we added interfaces and web service attributes.  We didn’t have the time to finish out all the entry points that would have completed this feature area so we’ve removed the web service attributes and interface implementation.  This doesn’t mean web services aren’t supported, nor any less important.  It just means we ship complete solutions that we can support in the future. 

For the TableAdapter interfaces, you can also still implement additional interfaces with the partial class support.  As we come up with some completed scenarios we’ll be posting samples and snippets for examples on how to extend these classes. 

TableAdapter Partial Classes

TableAdapters also support partial classes, however, theirs no UI gesture to automatically create this code.  Again, it was just a time thing.  If we generate the code, then you would also expect us to fix up the code when you rename the TableAdapters.  To add the partial class, you enter the code for the DataSet and enter code similar to the following:

Partial Public Class NorthwindDataSet

End Class

Namespace NorthwindDataSetTableAdapters

    Partial Public Class CategoriesTableAdapter


    End Class

End Namespace

Notice the namespace and the TableAdapters live outside the NorthwindDataSet class

Bridging the gap between CLR Null/Nothing and DBNull

When we added the object binding support it was obvious we still needed to populate these objects and send the changes back to the database.  Since we are no longer in denial that Null is a real scenario, J, we wanted to make sure developers didn’t have to write a lot of goofy code converting Null/Nothing to DBNull. 

TableAdapters will now support Nullable parameters on their type safe methods.

Typed Query Parameters

For TableAdapter queries we support Nullable parameters however we take a cautious approach.

Assume I've added two additional queries to the Order table.  FillByOrderDate and FillByShippedDate.  Since OrderDate doesn’t allow Null, it’s straight forward.  However, ShippedDate does allow Null values, so it’s a bit more complex.


Writing a SQL Query that supports Null is not as straight forward as one would think.

Can you catch what’s wrong with the following query?


SELECT Order_Id, OrderDate, ShippedDate, OrderAmount, CustomerPO, SalesEmployeeInitials
  FROM Order

 WHERE ShippedDate = @shippedDate


If @shippedDate is Null the query won’t return any results.  Null isn’t valid as a comparison.  The proper query would need to be written as the following:


SELECT Order_Id, OrderDate, ShippedDate, OrderAmount, CustomerPO, SalesEmployeeInitials

  FROM Order

 WHERE ShippedDate = @shippedDate

    OR (ShippedDate IS NULL AND @shippedDate IS NULL)


When the developer adds a TSQL query to the TableAdapter, such as the above query, we’ll default the parameters to AllowDBNull = False.  To change this, simply open the parameters collection within the property grid on the specific query within the TableAdapter and toggle it to AllowDBNull = True

When the parameter is AllowDBNull = False we’ll generate a method signature similar to the following:

Public Overridable Overloads Function FillByShippedDate(dataTable As NorthwindDataSet.OrderDataTable, _

    shippedDate As Date) As Integer

    Me.Adapter.SelectCommand = Me.CommandCollection(1)

    Me.Adapter.SelectCommand.Parameters(0).Value = CType(shippedDate, Date)

    If (Me.m_clearBeforeFill = True) Then


    End If

    Dim returnValue As Integer = Me.Adapter.Fill(dataTable)

    Return returnValue

End Function

When the parameter is AllowDBNull = True we’ll generate a method signature similar to the following:

Public Overridable Overloads Function FillByShippedDate(dataTable As NorthwindDataSet.OrderDataTable, _

    shippedDate As System.Nullable(Of Date)) As Integer

    Me.Adapter.SelectCommand = Me.CommandCollection(1)

    If (shippedDate.HasValue = True) Then

        Me.Adapter.SelectCommand.Parameters(0).Value = CType(shippedDate.Value, Date)


        Me.Adapter.SelectCommand.Parameters(0).Value = System.DBNull.Value

    End If


When the query is based on a Stored Procedure we’ll always set the parameter AllowDBNull to True as we can’t tell if the sproc supports null or not.  Again, you can toggle the parameter.AllowDBNull if you’d like your method signature to better represent what your sproc actually supports.

DBDirect Methods

DBDirect methods are type safe methods for executing insert, update or delete operations directly against the database.  You don’t need to construct a DataTable and pass it to dataAdapter with DataAdapter.Update.  They are especially useful when combining business objects and TableAdapters.


For the following Order table, we see several column types. 

Column Name


Allow Nulls




















In B2 the following method will be generated when calling the DBDirect Insert Method:

Note: the comments are added for explanation, but won’t be in the generated code.

Public Overridable Overloads Function Insert(Order_Id As Integer, _

    OrderDate As Date, _

    ShippedDate As System.Nullable(Of Date), _

    OrderAmount As System.Nullable(Of Decimal), _

    CustomerPO As String, _

    SalesEmployeeInitials As String) As Integer

    ' PK and OrderDate, no conversion necessary as they don't support Null

    Me.InsertCommand.Parameters(0).Value = CType(Order_Id, Integer)

    Me.InsertCommand.Parameters(1).Value = CType(OrderDate, Date)

    ' ShippedDate support Null. 

    ' However, the parameters object requires DBNull to pass to the database

    If (ShippedDate.HasValue = True) Then

        Me.InsertCommand.Parameters(2).Value = CType(ShippedDate.Value, Date)


        Me.InsertCommand.Parameters(2).Value = System.DBNull.Value

    End If

    Me.InsertCommand.Parameters(3).Value = CType(OrderAmount.Value, Decimal)

    ' CustomerPO is a string value which is a Reference type

    ' In B2, Reference types are no longer supported by Nullable(Of T)

    ' If Null is supported, we'll map Nothing to DBNull

    If (CustomerPO Is Nothing) Then

        Me.InsertCommand.Parameters(4).Value = System.DBNull.Value


        Me.InsertCommand.Parameters(4).Value = CType(CustomerPO, String)

    End If

    ' In this case Null isn't supported in the Database

    ' so we throw a ArgumentNullException with the column name

    If (SalesEmployeeInitials Is Nothing) Then

        Throw New System.ArgumentNullException("SalesEmployeeInitials")


        Me.InsertCommand.Parameters(5).Value = CType(SalesEmployeeInitials, String)

    End If



        Return Me.InsertCommand.ExecuteNonQuery



    End Try

End Function

We generate the parameter Nullability based on the Collumn.AllowDBNull which we originally read from the Database when configuring the TableAdapter/DataTable.


You may have read my blog on us removing the Transaction property from the TableAdapters.  This didn’t mean we cut support for Transactions, it just got easier.  In Whidbey there’s a new namespace in town, System.Transactions.  With these objects, it couldn’t have gotten any easier.

The System.Transactions namespace is smart enough to know when to scale out to DTC (Distributed Transaction Coordinator), and when to just use SQL Transactions.  If you’re using a single SQL 2005 instance, SQL Transactions will be used.  If you’re using two SQL servers, or 2 servers of a different flavor, System.Transactions will automatically enlist DTC into the operation.  You’re code doesn’t have to change.


Dim transactionScope As New System.Transactions.TransactionScope

Using transactionScope


      Me.Order_DetailsTableAdapter.Update( _


      Me.OrdersTableAdapter.Update( _




        Me.NorthwindDataSet.Orders.SuspendValidation = False

    End Try

End Using

Data Sources Window

Consolidation of Database and Local Data Source Types

In B1 the Data Source Wizard had two icons for Databases.  One for Local Data and one for Server databases.  This was a temporary place holder.  You can no connect to a database using the same Data Source Type.  The difference is in the ConnectionString you provide.

Miscellaneous stuff

Removing the Welcome page to the wizards

In order to streamline our wizards we’ve removed the Welcome page.  These pages are helpful the first time, but annoying each of the hundreds of subsequent times you run through the wizards.

Convert To Smart Tag Cut

In B1 data bound controls had a smart tag named Convert To.  While this was cool, it really wasn’t as complete as we would have liked.  It did convert the control, but it didn’t rename it, or link over all the events appropriately.  The UI for the dropdown didn’t really meet our guidelines.  The cost to complete this didn’t seem to justify the effort.  Since developers can simply change the Control type in the Data Sources Window, then drag it again, we felt this was a cleaner experience that freed us up to focus on other features


B2 to RTM

So, what’s left.  We still have a lot of stability issues to work out.  Please keep the bugs coming through LadyBug.  You’ve caught a lot of bugs that we might have either missed, or caught to late to fix it “properly.

DataSets as Data Sources in Referenced Assemblies

We still need to do some work on DataSets consumed as Data Sources from referenced assemblies.  In B2 we won’t instance the DataSet.  In RTM we’ll instance the DataSet just as we do when the DataSet is in the same assembly as the UI project.  This will solve a number of issues, including master details relationships.


We still have a lot of performance work left to make the DataSet designer, and all of VS run faster.

Web Services as Data Sources

There’s still some fit and finish work to make Web Services work properly in the Data Sources window. 


Steve Lasker
Program Manager

Visual Studio

Leave a Comment
  • Please add 3 and 4 and type the answer here:
  • Post
  • I may have missed this somewhere but when does beta 2 come out?
  • Excellent update Steve! Thank you.
  • I'm glad you are starting to deal with the null issue. I would like to know if there is a way to say that you allways want null columns in typed datasets to return the "empty" type. Currently you have to set it on each column in the XSD, and if something changes that causes you to have to recreate the XSD you have to remember to reset the property on each column.
    Thanks for the excellent update
  • Data Design accomplishments in VS 2005 Beta 2
  • At present we have to write templates (with CodeSmith) to generate all of the typed DataSets ourselves, as we require them to be all regenerated as part of the build process to match the schema in the database. (There is no point is having type safety at one level, if it does not automatically go end to end.)

    It would be good if all the DataSet designers wrote the options that the programmer chooses into a xml config file. This file should then be used along with the data schema by a command line tool to generate the typed DataSet. We could then just rerun this tool as part of our build process.

    ian @ ringrose dot name
Page 1 of 3 (44 items) 123