Return a DataTable from a WebMethod

I have an XML schema which defines my DataSet.  So, I deal with a MyDataSet type, which inherits from Data.DataSet, and a MyDataTable, which inherits from Data.DataTable.  These classes were created using the .NET IDE's "Generate Dataset" function, to create all these strongly-typed classes that mirror the structure of my XSD.

I have a WebMethod in a Web Service which I desperately want to return one of these MyDataTable objects as a value.  The MyDataTable object I want to return is part of a MyDataSet object--but I only want to return this single MyDataTable object.  My WinClient.EXE needs to receive this MyDataTable object, one way or another.  Here's what I've run up against so far:

[1]  My first idea was to have the WebMethod (let's call it WebFoo()) return a type of MyDataTable.  This seems the most obvious route.  But when I try to update the Web Reference to WebFoo() from my WinClient, then it complains about "no default constructor for MyDataTable".  I don't know how I would address this problem, so I move on.

[2]  Then I thought, hey, these DataSet classes (both base and derived) have a handy .GetXml() method, which claims to return String data representing the DataSet.  So I tried this in WebFoo():

   <WebMethod(MessageName:="WebFoo")> _
   Public Function WebFoo( _
      ByVal sSomeKey As String _
      ) _
      As String

      Dim oNewDataSet As New MyDataSet()
      '// First, we need to remove any (empty) table by the name of the one
      '// that we're about to add.  Otherwise, the Tables.Add() call below
      '// will blow sky-high.
      If (oNewDataSet.Tables.Contains(OldDataSet.MyDataTable.TableName)) Then
         Call oNewDataSet.Tables.Remove(OldDataSet.MyDataTable.TableName)
      End If

     '// If I don't use the .Clone() method here, execution fails with a
      '// message to the effect that, "MyDataTable is already a Table in
      '// the OldDataSet DataSet."
      '//    On the other hand, this .Clone method doesn't even include
      '// any rows--just the DataTable's structure.  Aaaaaugggh!
      Call oNewDataSet.Tables.Add(OldDataSet.MyDataTable.Clone)

      Dim sReturnValue As String = oNewRMXDataSet.GetXml()
      '// The only thing returned here is this:
      '//    <MyDataSet xmlns="" />
      Return sReturnValue

   End Function

See my comments in the above code--I'm hamstrung by [a] the Tables.Add() method (which seems to want the result of the DataTable.Clone() method), [b] the DataTable.Clone() method, which seems to ignore row data, and only clones the structure, and [c] the DataSet.GetXml() method, which does not return the whole DataSet, but only the first line thereof.

[3]  DarthPedro suggested I just return the MyDataSet object (since this would indeed have the default constructor necessary), but I still have to add the MyDataTable object to that return-value MyDataSet object before trying to return it, and I can't add MyDataTable to it because no rows get added.  Yeeesh.  I know I have to be making this harder than it really should be.

I guess I could loop through each row in the OldDataSet's MyDataTable, adding each one to the NewDataSet's MyDataTable, but at some point I have to think there's an easier (cleaner, more elegant) way of doing all this that I just don't know about.  Anyone out there know of such a way?

Thanks a lot.
Who is Participating?
I wear a lot of hats...

"The solutions and answers provided on Experts Exchange have been extremely helpful to me over the last few years. I wear a lot of hats - Developer, Database Administrator, Help Desk, etc., so I know a lot of things but not a lot about one thing. Experts Exchange gives me answers from people who do know a lot about one thing, in a easy to use platform." -Todd S.

Can you post your MyDataTable class code? Have you implemented Clone method  on it or are you relying on base class's implementation. Since its a strongly typed table, you may have to implement clone method but i am not sure about that. Also if you can post the schema file, then i can try duplcating your problem.
brykerAuthor Commented:

Sure.  Here's the schema, trimmed down to just include the one table that I want to return.  The other tables would seem to be extraneous to include here, so I clipped them just know that there ARE more tables than just this one.

   <?xml version="1.0" encoding="utf-8" ?>
   <xs:schema id="RMXDataModel" targetNamespace="" elementFormDefault="qualified"    xmlns="" xmlns:mstns="" xmlns:xs=""    xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" version="1.0">
      <xs:element name="BaseCaseInfo">
                        <xs:element name="CalibrationKey" type="xs:string" />
                        <xs:element name="BaseCaseKey" type="xs:string" />
                        <xs:element name="ModelKey" type="xs:string" />
                        <xs:element name="CalibrationState" type="xs:short" />
                        <xs:element name="BaseCaseType" type="xs:long" />
                        <xs:any namespace="##any" minOccurs="0" maxOccurs="unbounded" />
            <xs:key name="BaseCaseInfoKey" msdata:PrimaryKey="true">
                  <xs:selector xpath="." />
                  <xs:field xpath="mstns:CalibrationKey" />

The DataTable class code:

    <System.Diagnostics.DebuggerStepThrough()>  _
    Public Class BaseCaseInfoDataTable
        Inherits DataTable
        Implements System.Collections.IEnumerable
        Private columnCalibrationKey As DataColumn
        Private columnBaseCaseKey As DataColumn
        Private columnModelKey As DataColumn
        Private columnCalibrationState As DataColumn
        Private columnBaseCaseType As DataColumn
        Friend Sub New()
        End Sub
        Friend Sub New(ByVal table As DataTable)
            If (table.CaseSensitive <> table.DataSet.CaseSensitive) Then
                Me.CaseSensitive = table.CaseSensitive
            End If
            If (table.Locale.ToString <> table.DataSet.Locale.ToString) Then
                Me.Locale = table.Locale
            End If
            If (table.Namespace <> table.DataSet.Namespace) Then
                Me.Namespace = table.Namespace
            End If
            Me.Prefix = table.Prefix
            Me.MinimumCapacity = table.MinimumCapacity
            Me.DisplayExpression = table.DisplayExpression
        End Sub
        <System.ComponentModel.Browsable(false)>  _
        Public ReadOnly Property Count As Integer
                Return Me.Rows.Count
            End Get
        End Property
        Friend ReadOnly Property CalibrationKeyColumn As DataColumn
                Return Me.columnCalibrationKey
            End Get
        End Property
        Friend ReadOnly Property BaseCaseKeyColumn As DataColumn
                Return Me.columnBaseCaseKey
            End Get
        End Property
        Friend ReadOnly Property ModelKeyColumn As DataColumn
                Return Me.columnModelKey
            End Get
        End Property
        Friend ReadOnly Property CalibrationStateColumn As DataColumn
                Return Me.columnCalibrationState
            End Get
        End Property
        Friend ReadOnly Property BaseCaseTypeColumn As DataColumn
                Return Me.columnBaseCaseType
            End Get
        End Property
        Public Default ReadOnly Property Item(ByVal index As Integer) As BaseCaseInfoRow
                Return CType(Me.Rows(index),BaseCaseInfoRow)
            End Get
        End Property
        Public Event BaseCaseInfoRowChanged As BaseCaseInfoRowChangeEventHandler
        Public Event BaseCaseInfoRowChanging As BaseCaseInfoRowChangeEventHandler
        Public Event BaseCaseInfoRowDeleted As BaseCaseInfoRowChangeEventHandler
        Public Event BaseCaseInfoRowDeleting As BaseCaseInfoRowChangeEventHandler
        Public Overloads Sub AddBaseCaseInfoRow(ByVal row As BaseCaseInfoRow)
        End Sub
        Public Overloads Function AddBaseCaseInfoRow(ByVal CalibrationKey As String, ByVal BaseCaseKey As String, ByVal ModelKey As String, ByVal CalibrationState As Short, ByVal BaseCaseType As Long) As BaseCaseInfoRow
            Dim rowBaseCaseInfoRow As BaseCaseInfoRow = CType(Me.NewRow,BaseCaseInfoRow)
            rowBaseCaseInfoRow.ItemArray = New Object() {CalibrationKey, BaseCaseKey, ModelKey, CalibrationState, BaseCaseType}
            Return rowBaseCaseInfoRow
        End Function
        Public Function GetEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
            Return Me.Rows.GetEnumerator
        End Function
        Public Overrides Function Clone() As DataTable
            Dim cln As BaseCaseInfoDataTable = CType(MyBase.Clone,BaseCaseInfoDataTable)
            Return cln
        End Function
        Protected Overrides Function CreateInstance() As DataTable
            Return New BaseCaseInfoDataTable
        End Function
        Friend Sub InitVars()
            Me.columnCalibrationKey = Me.Columns("CalibrationKey")
            Me.columnBaseCaseKey = Me.Columns("BaseCaseKey")
            Me.columnModelKey = Me.Columns("ModelKey")
            Me.columnCalibrationState = Me.Columns("CalibrationState")
            Me.columnBaseCaseType = Me.Columns("BaseCaseType")
        End Sub
        Private Sub InitClass()
            Me.columnCalibrationKey = New DataColumn("CalibrationKey", GetType(System.String), Nothing, System.Data.MappingType.Element)
            Me.columnBaseCaseKey = New DataColumn("BaseCaseKey", GetType(System.String), Nothing, System.Data.MappingType.Element)
            Me.columnModelKey = New DataColumn("ModelKey", GetType(System.String), Nothing, System.Data.MappingType.Element)
            Me.columnCalibrationState = New DataColumn("CalibrationState", GetType(System.Int16), Nothing, System.Data.MappingType.Element)
            Me.columnBaseCaseType = New DataColumn("BaseCaseType", GetType(System.Int64), Nothing, System.Data.MappingType.Element)
            Me.columnCalibrationKey.AllowDBNull = false
            Me.columnBaseCaseKey.AllowDBNull = false
            Me.columnModelKey.AllowDBNull = false
            Me.columnCalibrationState.AllowDBNull = false
            Me.columnBaseCaseType.AllowDBNull = false
        End Sub
        Public Function NewBaseCaseInfoRow() As BaseCaseInfoRow
            Return CType(Me.NewRow,BaseCaseInfoRow)
        End Function
        Protected Overrides Function NewRowFromBuilder(ByVal builder As DataRowBuilder) As DataRow
            Return New BaseCaseInfoRow(builder)
        End Function
        Protected Overrides Function GetRowType() As System.Type
            Return GetType(BaseCaseInfoRow)
        End Function
        Protected Overrides Sub OnRowChanged(ByVal e As DataRowChangeEventArgs)
            If (Not (Me.BaseCaseInfoRowChangedEvent) Is Nothing) Then
                RaiseEvent BaseCaseInfoRowChanged(Me, New BaseCaseInfoRowChangeEvent(CType(e.Row,BaseCaseInfoRow), e.Action))
            End If
        End Sub
        Protected Overrides Sub OnRowChanging(ByVal e As DataRowChangeEventArgs)
            If (Not (Me.BaseCaseInfoRowChangingEvent) Is Nothing) Then
                RaiseEvent BaseCaseInfoRowChanging(Me, New BaseCaseInfoRowChangeEvent(CType(e.Row,BaseCaseInfoRow), e.Action))
            End If
        End Sub
        Protected Overrides Sub OnRowDeleted(ByVal e As DataRowChangeEventArgs)
            If (Not (Me.BaseCaseInfoRowDeletedEvent) Is Nothing) Then
                RaiseEvent BaseCaseInfoRowDeleted(Me, New BaseCaseInfoRowChangeEvent(CType(e.Row,BaseCaseInfoRow), e.Action))
            End If
        End Sub
        Protected Overrides Sub OnRowDeleting(ByVal e As DataRowChangeEventArgs)
            If (Not (Me.BaseCaseInfoRowDeletingEvent) Is Nothing) Then
                RaiseEvent BaseCaseInfoRowDeleting(Me, New BaseCaseInfoRowChangeEvent(CType(e.Row,BaseCaseInfoRow), e.Action))
            End If
        End Sub
        Public Sub RemoveBaseCaseInfoRow(ByVal row As BaseCaseInfoRow)
        End Sub
    End Class

Let me know if I can include anything else for you.


    <WebMethod(MessageName:="WebFoo")> _
     Public Function WebFoo( _
        ByVal sSomeKey As String _
        ) _
        As DataSet

        Dim OriginalDS As New RMXDataModel()
        Dim ResultDS As New DataSet()
        '  fill table in originalds

        '  copy to a generic dataset and return
        Return ResultDS
    End Function
    ' the user of this function will find the table in
    ' webfoo("0").Tables(0)
    ' if you want them to have the same functionality
    '  you have while using a typed dataset
    '  there are a couple things you ought to be doing
    '  I can explain them if you decide to go this way
Angular Fundamentals

Learn the fundamentals of Angular 2, a JavaScript framework for developing dynamic single page applications.

brykerAuthor Commented:

Thanks for the response.

[1] I haven't tried your code yet, but how does ResultDS get populated in the code you suggest?  I just see you allocating ResultDS, but not how it gets populated by the original's BaseCaseInfo table.

[2] Yes, I'd like to return a typed dataset.  I'm convinced there's got to be some way to do that...
You could create another typed dataset, simplified to the single table that you want to return.  Populate either through a query or looping through your existing table.  Then, return that newly typed dataset.
brykerAuthor Commented:
Wouldn't another typed dataset require another (different) XSD?
brykerAuthor Commented:
Okay, maybe I'm onto something: the DataTable.Copy() method:

   DataTable.Copy Method  [Visual Basic]

   Copies both the structure and data for this DataTable.

Now, maybe we can figure out how to get this added to a typed DataSet, and returned back to the WinClient correctly.  More to come...
brykerAuthor Commented:
Well, my WebMethod is now return=typed to return that typed DataSet, but my WinClient fails when I try to use this kind of call:

oRMXDataSet = CType(MyWebService.WebFoo(sKey), RMXDataModelLib.RMXDataModel)

Error text:

Value of type '' cannot be converted to 'RMXDataModelLib.RMXDataModel'.

wow, that's gnarly looking...  :)

It looks like it's detecting that the typed DataSets are different and not allowing the conversion to happen.  How do you fix that?  No idea.

So, are you using the same typed dataset for your full data representation (with a bunch of tables) and the new one that your returning (with just the single table)?  I'm afraid that that might be the problem because when it's deserializing the data returned from the web service, it's expecting dataset to have the full representation.
brykerAuthor Commented:
Well, but that's what's funny about instantiating these new MyDataSet objects--after creating a new one, I have to remove the BaseCaseInfo table that it ALREADY HAS (which would be structure-only, of course) before I add in my POPULATED one.  That made sense when I finally saw that.  

But what that means is that both Datasets (the fully-populated one and the one with just one table populated) are identical in structure--the newer one would only have a single table populated, but the other tables are there (in stucture only).  So these 2 DataSets ought to be no different than say, two structs, one with initialized values and one without.  The fact that I have to remove a table from a just-allocated MyDataSet object tells me these things ought to be identical in their types, just not in their rowcounts.  Know what I mean?
Take a look at the DataSet.Merge (DataTable) method.  This method allows you to merge a DataTable (both the structure AND the data) into a DataSet.

Regarding question [1] in your original post, for some reason, MS does not provide a serialization mechanism for the DataTable, therefore there's no way for you to return a DataTable object from a web service.  As you've (painfully) discovered, the only way to return the DataTable is to wrap a DataSet around it.

Experts Exchange Solution brought to you by

Your issues matter to us.

Facing a tech roadblock? Get the help and guidance you need from experienced professionals who care. Ask your question anytime, anywhere, with no hassle.

Start your 7-day free trial
Here's an article which might be helpful.  It discusses the issues of passing typed objects between 2 web services, but it also might be appropriate for your situation.
brykerAuthor Commented:
Finally finding the DataTable.Copy() method got me to the point where I could get the whole table (structure and data) into the DataSet.

So, at the moment, I'm just doing that, then sending the DataSet.GetXml() string back from my Web Service.  So I got past that, though I'd really still like to exchange typed DataTables.

So, I'll take a look at your article.  Thanks.
brykerAuthor Commented:
Never got typed-table exchanging down, but this question has been hanging around long enough.  My finding that I needed to use .Copy(), not .Clone() was very helpful, but some of what Coyote provided was also helpful, so I'm going to send the points his way.

oops--EE is giving me some weird error when trying to accept Coyote's comments as the answer.  Will try to accept his answer later.
brykerAuthor Commented:
Sorry for the delay in grading this question.  I had to post a question in Community Support documenting an error that occurred every time I tried to award points.  Seems to be fixed now.

It's more than this solution.Get answers and train to solve all your tech problems - anytime, anywhere.Try it for free Edge Out The Competitionfor your dream job with proven skills and certifications.Get started today Stand Outas the employee with proven skills.Start learning today for free Move Your Career Forwardwith certification training in the latest technologies.Start your trial today
.NET Programming

From novice to tech pro — start learning today.