Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro Visual C++-CLI And The .NET 2.0 Platform (2006) [eng]-1

.pdf
Скачиваний:
70
Добавлен:
16.08.2013
Размер:
24.18 Mб
Скачать

648 C H A P T E R 1 5 W E B S E R V I C E S

Once the wizard has done its thing, you need to add a Web reference to the FindZipCode Web service. I thought it would be neat to use a real Web reference from over the Internet instead of localhost, so I copied the Web service FindZipCode that I created previously to my Web site host server, ProCppCLI.net. Unfortunately, ProCppCLI.net does not support .NET Framework version 2.0 yet, so I had to use an old .NET Framework version 1.1 copy of the Web service from ManagedCpp.net (which is an old site of mine), but the result is ultimately the same.

Tip For those of you who want to try out a remote copy of the Web service, I keep a copy of FindZipCode on an old Web site of mine: www.ManagedCpp.net. You can find the Web service at http://www.managedcpp.net/ FindZipCode/FindZipCode.asmx.

To add a Web reference, you right-click the References folder of your client application and select Add Web Reference. This will cause the Add Web Reference dialog box to appear (see Figure 15-5).

Figure 15-5. The Add Web Reference dialog box that appears before you select a Web service

From here, you can either click one of the links within the dialog box to search for the Web service or type the URL of the Web service in the supplied text box. In Figure 15-5, I typed in the URL of the Web service, but if you don’t have access to a Web server or don’t want to use my copy of the Web service, then select the “Web services on the local machine” link, which will find and make available the Web service you built previously. Once you select the Web service, you want the Add Web Reference dialog box changes to look like Figure 15-6.

C H A P T E R 1 5 W E B S E R V I C E S

649

Figure 15-6. The Add Web Reference dialog box after you have selected a Web service

Now all you have to do is change the Web reference name to something more appropriate than the Web server’s name, and then click the Add Reference button.

The addition of a Web reference adds a number of files to your project. Among them are a WSDL file and a DISCO file. Both are nice to look at but, in most cases, you will do nothing with them directly. The only file of real importance is the include file with the same name as the Web reference name you changed previously. All you need to do with this file is include it at the top of your client application. If you are curious, you can open this file to see some of the details of how the connection to the Web service is made.

Now you need to make the changes to your main .cpp file, as shown in Listing 15-10.

Listing 15-10. A Console Web Services Client Application

#include "FindZipCode.h"

using namespace System;

void main()

{

FindZipCode::FindZipCodeClass ^fzc = gcnew FindZipCode::FindZipCodeClass();

try

{

Console::WriteLine(fzc->GetZip("Louisville", "KY").ToString()); Console::WriteLine(fzc->GetZip("San Jose", "CA").ToString()); Console::WriteLine(fzc->GetZip("xx", "cc").ToString());

}

650 C H A P T E R 1 5 W E B S E R V I C E S

catch (Exception ^e)

{

Console::WriteLine(e->Message);

}

}

Believe it or not, that’s all the coding you have to do. Notice that you instantiate a Web service class in the same way as you do any other class:

FindZipCode::FindZipCodeClass ^fzc = gcnew FindZipCode::FindZipCodeClass();

Also notice that you access methods in the same way:

fzc->GetZip("Louisville", "KY").ToString();

From the client programming perspective, there is no difference between using a local class and using a Web service class. If I were to give this code to a developer, he would have no way of knowing it uses Web services unless he or she opened the FindZipCode.h include file.

Go ahead and run the client. Figure 15-7 shows the result of the client application ZipCodeConsoleClient.exe. As is expected, two zip codes are printed to the console, and then the exception is captured and printed to the console (just as I predicted at the beginning of this example).

Figure 15-7. The client consumer of Web service FindZipCode in action

Debugging a Web Service

Debugging a Web service on its own is really no different than debugging any other .NET application. Simply compile within the debug solution configuration and then set breakpoints where you want the execution to stop.

There is only one scenario that requires you to do anything special, and that is if you create your Web service within a solution that has a different name than the Web service. When you do this, starting the debugger causes the error shown in Figure 15-8.

Figure 15-8. The debugging Web service error

The wording of the error doesn’t really explain what caused the error. But it’s very easy to solve the problem. What has happened is that the URL to your Web service is incorrect. When you build a Web service from an existing solution, Visual Studio 2005 creates a URL to your Web service using the solution’s path instead of the Web service’s. Thus, if you look in the Web service project properties under the Configuration Properties Debugging folder, you’ll find that the HTTP URL has a value of

C H A P T E R 1 5 W E B S E R V I C E S

651

http://localhost/solutiondir/webservicename.asmx when it should have the value http://localhost/ webservicedir/webservicename.asmx. To fix the problem, simply type the correct HTTP URL in the text box.

Debugging a Web service when it is being consumed by a client is, on the other hand, not as simple and could require a little more effort to set up, depending on how your environment is set up.

The first scenario is when the client and Web service are in two different solutions. If this is the case, simply start up the Web service solution in debug mode and when the client calls the Web service the breakpoint will be triggered.

The second scenario is when the client and Web service are in the same solution but in different projects. In this case, I use the following two procedures (there are probably others that I don’t know).

Procedure 1

The first step is to set a breakpoint in the client calling the Web service just before the first time you want to call the Web service with the debugger.

Then, the only way you can get the debugger to work within the Web service is to step into the Web service. Once you have stepped into Web service, from then on you can debug the Web service just like any other part of the application. In other words, breakpoints within the Web service don’t work unless you step into the Web service at least once first.

Procedure 2

Open up two instances of Visual Studio 2005 for the solution. One will open a dialog box (see Figure 15-9) stating that the .ncb file could not be opened for writing.

Figure 15-9. The .ncb file cannot be edited by multiple concurrent instances of Visual Studio.

Remember which instance generated this error, as you don’t want to make any modification to the code with this instance of Visual Studio 2005.

Now, in either instance start up the Web service in debug mode. Once it has started, open up the client in the other instance. Now when a call to the Web service is made the first instance of Visual Studio 2005 will stop at any breakpoints you may have set up (without needing to step into it like you did in the first procedure).

Passing Data Using a Web Service

I’m going to finish this chapter with a more elaborate example of a Web service. It will take the MaintAuthors detached database project example you created back in Chapter 12 and convert it to a Web service.

With this example, you will truly see a detached (figuratively speaking) database, where the client is on one system and the Web service (database) is located somewhere else on the Internet.

The Web service consists of two methods. The first returns a DataSet of authors, and the second takes in a DataSet of authors and updates the database based on the batched processes made by the client to the authors DataSet. You should note that this example considers no concurrency issues

652 C H A P T E R 1 5 W E B S E R V I C E S

(i.e., what happens if multiple clients update the database via the multiple Web service instances at the same time?).

The Windows Form client application receives a DataSet of authors and then allows additions, updates, and deletions to the DataSet.

Using Web Service GUI Designer Tool

One neat feature of Visual Studio 2005 is the ability to drag and drop a SqlConnection to a Web service. Not only does this spare you the time and effort of writing the code for the SqlConnection, but it also saves you from having to figure out the connection string to connect to a database.

The steps are… actually, the step is quite simple. Directly out of the Server Explorer, drag the data connection to the database you want to add a connection to, and drop it on the designer screen of the web service’s .h file. Once you do that, the code needed to create a SqlConnection is automatically added to the actual .h file. Listing 15-11 shows the auto-generated code (both by the project template and by dropping and dragging a connection) along with the definition of the two Web service methods needed to implement the detached database.

Listing 15-11. AuthorWSClass.h

using namespace System;

using namespace System::Data;

using namespace System::Data::SqlClient; using namespace System::Web;

using namespace System::Web::Services;

namespace AuthorWS

{

[WebServiceBinding(ConformsTo=WsiProfiles::BasicProfile1_1, EmitConformanceClaims = true)]

[WebService(Namespace="http://procppcli.net",

Description = "Author table access Web service")]

public ref class AuthorWSClass : public System::Web::Services::WebService

{

public:

AuthorWSClass()

{

InitializeComponent();

}

protected:

~AuthorWSClass()

{

if (components)

{

delete components;

}

}

private: System::Data::SqlClient::SqlConnection^ sqlConnection; private: System::ComponentModel::IContainer^ components;

C H A P T E R 1 5 W E B S E R V I C E S

653

private:

void InitializeComponent()

{

this->sqlConnection =

gcnew System::Data::SqlClient::SqlConnection();

//

// sqlConnection

//

this->sqlConnection->ConnectionString = L"Server=Amidala;Integrated Security=True;Database=DCV_DB";

this->sqlConnection->FireInfoMessageEventOnUserErrors = false;

}

public: [WebMethod(Description =

"Method to retrieve All Authors from the database")] DataSet ^GetAuthors();

[WebMethod(Description =

"Method to Commit changed made on client with Server database")] void UpdateAuthors(DataSet ^dSet);

};

}

Returning a DataSet

The easier of the two Web service methods to implement relates to filling a DataSet of all authors and then sending the DataSet from the Web service to the consuming client (see Listing 15-12).

Listing 15-12. Building the Authors DataSet Web Service

DataSet^ AuthorWSClass::GetAuthors()

{

SqlDataAdapter ^dAdapt; DataSet ^dSet;

dAdapt = gcnew SqlDataAdapter();

dAdapt->MissingSchemaAction = MissingSchemaAction::AddWithKey;

dAdapt->SelectCommand =

gcnew SqlCommand("SELECT AuthorID, LastName, FirstName FROM Authors", sqlConnection);

dSet = gcnew DataSet(); dAdapt->Fill(dSet, "Authors");

return dSet;

}

As you can see, a Web service has no problems sending the complex DataSet object using SOAP. In fact, if it wasn’t for the WebMethod attribute found in the method’s declaration, this method would look like any other ADO.NET DataSet fill method.

654 C H A P T E R 1 5 W E B S E R V I C E S

One big difference, though, is that this method uses its own method scope version of the SqlConnection (auto-generated), DataAdapter, and DataSet. The reason is that a Web service (unless otherwise specified using the EnableSession property of the WebMethod attribute) is stateless. Basically, each time the Web service is called, it is from scratch. Thus, there is no need to have the SqlConnection, DataAdapter, or DataSet stick around after the Web service method has finished. For this same reason, there is no reason to assign the InsertCommand, UpdateCommand, and DeleteCommand properties to the DataAdapter as they are not used in the method.

Inserting, Updating, and Deleting Rows in a DataSet

Inserting, updating, and deleting rows in a DataSet via a Web service is handled in virtually the same way as standard, nondistributed ADO.NET. The UpdateAuthors() method (see Listing 15-13) is made up of code that is almost exactly the same as what you saw in Chapter 12.

Listing 15-13. Updating the Authors Database Web Service

void AuthorWSClass::UpdateAuthors(DataSet ^dSet)

{

SqlDataAdapter ^dAdapt;

dAdapt = gcnew SqlDataAdapter();

dAdapt->MissingSchemaAction = MissingSchemaAction::AddWithKey;

dAdapt->InsertCommand =

gcnew SqlCommand("INSERT INTO Authors (LastName, FirstName) " "VALUES (@LastName, @FirstName)", sqlConnection);

dAdapt->InsertCommand->Parameters->Add("@LastName", SqlDbType::VarChar, 50, "LastName");

dAdapt->InsertCommand->Parameters->Add("@FirstName", SqlDbType::VarChar, 50, "FirstName");

dAdapt->UpdateCommand =

gcnew SqlCommand("UPDATE Authors SET LastName = @LastName," "FirstName = @FirstName "

"WHERE AuthorID = @AuthorID", sqlConnection);

dAdapt->UpdateCommand->Parameters->Add("@LastName", SqlDbType::VarChar, 50, "LastName");

dAdapt->UpdateCommand->Parameters->Add("@FirstName", SqlDbType::VarChar, 50, "FirstName");

dAdapt->UpdateCommand->Parameters->Add("@AuthorID", SqlDbType::Int, 4, "AuthorID");

dAdapt->DeleteCommand =

gcnew SqlCommand("DELETE FROM Authors WHERE AuthorID = @AuthorID", sqlConnection);

dAdapt->DeleteCommand->Parameters->Add("@AuthorID", SqlDbType::Int, 4, "AuthorID");

dAdapt->Update(dSet, "Authors");

}

C H A P T E R 1 5 W E B S E R V I C E S

655

I’m sure you are seeing the pattern here. Distributed code using Web services is usually very close to, if not the same as, its nondistributed equivalent. The only real difference is that the class state is not maintained. Therefore, you have to be careful about global and class variables.

Unlike the plain ADO.NET version in Chapter 12, the Web service creates a new version of the DataAdapter each time a DataSet update is required. The reason, as I stated previously, is that the Web service is stateless, so on the call to the AuthorUpdate() method, no DataAdapter object exists. Having a new or different DataAdapter from the one when the DataSet was created is not an issue, because a DataAdapter is not strongly linked to the DataSet it is supporting. In fact, as long as the database schema is the same, DataSets are interchangeable as far as DataAdapters are concerned. As you will see later, the DataSet of the Update process can be a subset of the one sent by the GetAuthors() method, because only changed rows are contained within this DataSet.

What is neat about this method is that it can handle inserted, updated, and deleted records, all in a batch-like manner, instead of requiring a separate method for each of these process types.

Caution To simplify this example, I didn’t add any code to handle database concurrency.

One major issue that you may encounter when you try to access a database from within a Web service is that the Web service does not have the rights to access it. Instead you get the following error:

Exception Details: System.Data.SqlClient.SqlException: Login failed for user 'COMPUTERNAME\ASPNET'.

What this means in layman terms is that the Web service logs in to the database using the login ID of COMPUTERNAME\ASPNET and not your login ID. Thus, if the database is not set up to accept this login ID, then things don’t go very well for your Web service.

The solution is simple (once you know it). Add COMPUTERNAME\ASPNET as a user who can log in to the database in question. To do this, you need to run the following commands (I use SQL Query Analyzer but you can use the command osql in a command window as well):

USE DATABASENAME

EXEC sp_grantlogin 'COMPUTERNAME\ASPNET' EXEC sp_grantdbaccess 'COMPUTERNAME\ASPNET'

EXEC sp_addrolemember 'db_owner', 'COMPUTERNAME\ASPNET' go

where COMPUTERNAME is the name of the computer the Web service is running on.

Authors DataSet Processing Web Service Client

In truth, there is little reason to include this section in the chapter other than to show that very little has changed in the Web service client application when you compare it to the ADO.NET example in Chapter 12. Listing 15-14 has been included so that you can compare it to the source code of the MaintAuthors example in Chapter 12.

656 C H A P T E R 1 5 W E B S E R V I C E S

Listing 15-14. Web Server Version of the MaintAuthors Application

namespace

MaintAuthors

{

 

using

namespace System;

using

namespace System::ComponentModel;

using

namespace System::Collections;

using

namespace System::Windows::Forms;

using

namespace System::Data;

using

namespace System::Drawing;

public ref class Form1 : public System::Windows::Forms::Form

{

public:

Form1(void)

{

InitializeComponent();

authors = gcnew AuthorWS::AuthorWSClass(); dSet = authors->GetAuthors();

DataTable ^dt = dSet->Tables["Authors"];

if (dt == nullptr)

throw gcnew Exception("No Authors Table");

for each (DataRow ^row in dt->Rows::get())

{

lbAuthors->Items->Add(ListBoxItem(row));

}

CurrentAuthorID = -1;

}

protected:

~Form1()

{

if (components)

{

delete components;

}

}

DataSet ^dSet;

int CurrentAuthorID; AuthorWS::AuthorWSClass ^authors;

void InitializeComponent(void) //... Not shown to save space

C H A P T E R 1 5 W E B S E R V I C E S

657

private:

String ^ListBoxItem(DataRow ^row)

{

return String::Format("{0} {1} {2}", row["AuthorID"], row["FirstName"], row["LastName"]);

}

System::Void bnRollback_Click(System::Object^ sender, System::EventArgs^ e)

{

dSet->RejectChanges();

lbAuthors->Items->Clear();

DataTable ^dt = dSet->Tables["Authors"];

for each (DataRow^ row in dt->Rows)

{

lbAuthors->Items->Add(ListBoxItem(row));

}

CurrentAuthorID = -1;

}

System::Void bnCommit_Click(System::Object^ sender, System::EventArgs^ e)

{

authors->UpdateAuthors(dSet->GetChanges()); dSet->AcceptChanges();

lbAuthors->Items->Clear();

DataTable ^dt = dSet->Tables["Authors"];

for each (DataRow^ row in dt->Rows)

{

lbAuthors->Items->Add(ListBoxItem(row));

}

CurrentAuthorID = -1;

}

System::Void bnDelete_Click(System::Object^ sender, System::EventArgs^ e)

{

if (CurrentAuthorID < 0) return;

DataTable ^dt = dSet->Tables["Authors"]; array<DataRow^>^ row =

dt->Select(String::Format("AuthorID={0}", CurrentAuthorID)); row[0]->Delete();