Solved

Using Sharepoint Web Services in a delphi application.

Posted on 2009-05-17
10
3,028 Views
Last Modified: 2013-11-23
I am trying to add users and affect group memberships with a delphi application.  I can currently get the xml results from any function that does not require input, but if i try to add a user or list users from within a group, such as the GetUserCollectionFromGroup function, i get a SOAP Error, though i do not get any detailed error messages.  I am pretty sure that this is a input format issue as i cannot get back from a simple group name, while i can get a list of users from the entire site.

attached first is the code from the import of the WSDL of the UserGroup Web Service, then (Starting on line 191) the code from my test application to pull a list of users in the group 'AWP Members'
// ************************************************************************ //
// The types declared in this file were generated from data read from the
// WSDL File described below:
// WSDL     : https://mawp.securespsites.com/_vti_bin/usergroup.asmx?WSDL
// (5/11/2009 4:58:46 PM - 1.33.2.5)
// ************************************************************************ //
 
unit shSharePointWS_UserGroup;
 
interface
 
uses InvokeRegistry, SOAPHTTPClient, Types, XSBuiltIns;
 
type
 
  // ************************************************************************ //
  // The following types, referred to in the WSDL document are not being represented
  // in this file. They are either aliases[@] of other types represented or were referred
  // to but never[!] declared in the document. The types from the latter category
  // typically map to predefined/known XML or Borland types; however, they could also
  // indicate incorrect WSDL documents that failed to declare or import a schema type.
  // ************************************************************************ //
  // !:string          - "http://www.w3.org/2001/XMLSchema"
  // !:usersInfoXml    - "http://www.w3.org/2001/XMLSchema"
  // !:userLoginNamesXml - "http://www.w3.org/2001/XMLSchema"
  // !:int             - "http://www.w3.org/2001/XMLSchema"
  // !:unsignedLong    - "http://www.w3.org/2001/XMLSchema"
 
 
  GetUserCollectionFromSiteResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetUserCollectionFromWebResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetAllUserCollectionFromWebResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetUserCollectionFromGroupResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetUserCollectionFromRoleResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  userLoginNamesXml =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetUserCollectionResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetUserInfoResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetGroupCollectionFromSiteResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetGroupCollectionFromWebResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetGroupCollectionFromRoleResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetGroupCollectionFromUserResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  groupNamesXml   =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetGroupCollectionResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetGroupInfoResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetRoleCollectionFromWebResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetRoleCollectionFromGroupResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetRoleCollectionFromUserResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  roleNamesXml    =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetRoleCollectionResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetRoleInfoResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  emailXml        =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetUserLoginFromEmailResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetRolesAndPermissionsForCurrentUserResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  GetRolesAndPermissionsForSiteResult =  type WideString;      { "http://schemas.microsoft.com/sharepoint/soap/directory/" }
  usersInfoXml                        = type WideString;
 
  // ************************************************************************ //
  // Namespace : http://schemas.microsoft.com/sharepoint/soap/directory/
  // soapAction: http://schemas.microsoft.com/sharepoint/soap/directory/%operationName%
  // transport : http://schemas.xmlsoap.org/soap/http
  // binding   : UserGroupSoap
  // service   : UserGroup
  // port      : UserGroupSoap
  // URL       : https://mawp.securespsites.com/_vti_bin/usergroup.asmx
  // ************************************************************************ //
  UserGroupSoap = interface(IInvokable)
  ['{960EBC8B-8FB0-AFA4-4BD5-55B26C7987B3}']
    function  GetUserCollectionFromSite: GetUserCollectionFromSiteResult; stdcall;
    function  GetUserCollectionFromWeb: GetUserCollectionFromWebResult; stdcall;
    function  GetAllUserCollectionFromWeb: GetAllUserCollectionFromWebResult; stdcall;
    function  GetUserCollectionFromGroup(const groupName: WideString): GetUserCollectionFromGroupResult; stdcall;
    function  GetUserCollectionFromRole(const roleName: WideString): GetUserCollectionFromRoleResult; stdcall;
    function  GetUserCollection(const userLoginNamesXml: userLoginNamesXml): GetUserCollectionResult; stdcall;
    function  GetUserInfo(const userLoginName: WideString): GetUserInfoResult; stdcall;
    procedure AddUserToGroup(const groupName: WideString; const userName: WideString; const userLoginName: WideString; const userEmail: WideString; const userNotes: WideString); stdcall;
    procedure AddUserCollectionToGroup(const groupName: WideString; const usersInfoXml: usersInfoXml); stdcall;
    procedure AddUserToRole(const roleName: WideString; const userName: WideString; const userLoginName: WideString; const userEmail: WideString; const userNotes: WideString); stdcall;
    procedure AddUserCollectionToRole(const roleName: WideString; const usersInfoXml: usersInfoXml); stdcall;
    procedure UpdateUserInfo(const userLoginName: WideString; const userName: WideString; const userEmail: WideString; const userNotes: WideString); stdcall;
    procedure RemoveUserFromSite(const userLoginName: WideString); stdcall;
    procedure RemoveUserCollectionFromSite(const userLoginNamesXml: userLoginNamesXml); stdcall;
    procedure RemoveUserFromWeb(const userLoginName: WideString); stdcall;
    procedure RemoveUserFromGroup(const groupName: WideString; const userLoginName: WideString); stdcall;
    procedure RemoveUserCollectionFromGroup(const groupName: WideString; const userLoginNamesXml: userLoginNamesXml); stdcall;
    procedure RemoveUserFromRole(const roleName: WideString; const userLoginName: WideString); stdcall;
    procedure RemoveUserCollectionFromRole(const roleName: WideString; const userLoginNamesXml: userLoginNamesXml); stdcall;
    function  GetGroupCollectionFromSite: GetGroupCollectionFromSiteResult; stdcall;
    function  GetGroupCollectionFromWeb: GetGroupCollectionFromWebResult; stdcall;
    function  GetGroupCollectionFromRole(const roleName: WideString): GetGroupCollectionFromRoleResult; stdcall;
    function  GetGroupCollectionFromUser(const userLoginName: WideString): GetGroupCollectionFromUserResult; stdcall;
    function  GetGroupCollection(const groupNamesXml: groupNamesXml): GetGroupCollectionResult; stdcall;
    function  GetGroupInfo(const groupName: WideString): GetGroupInfoResult; stdcall;
    procedure AddGroup(const groupName: WideString; const ownerIdentifier: WideString; const ownerType: WideString; const defaultUserLoginName: WideString; const description: WideString); stdcall;
    procedure AddGroupToRole(const roleName: WideString; const groupName: WideString); stdcall;
    procedure UpdateGroupInfo(const oldGroupName: WideString; const groupName: WideString; const ownerIdentifier: WideString; const ownerType: WideString; const description: WideString); stdcall;
    procedure RemoveGroup(const groupName: WideString); stdcall;
    procedure RemoveGroupFromRole(const roleName: WideString; const groupName: WideString); stdcall;
    function  GetRoleCollectionFromWeb: GetRoleCollectionFromWebResult; stdcall;
    function  GetRoleCollectionFromGroup(const groupName: WideString): GetRoleCollectionFromGroupResult; stdcall;
    function  GetRoleCollectionFromUser(const userLoginName: WideString): GetRoleCollectionFromUserResult; stdcall;
    function  GetRoleCollection(const roleNamesXml: roleNamesXml): GetRoleCollectionResult; stdcall;
    function  GetRoleInfo(const roleName: WideString): GetRoleInfoResult; stdcall;
    procedure AddRole(const roleName: WideString; const description: WideString; const permissionMask: Integer); stdcall;
    procedure AddRoleDef(const roleName: WideString; const description: WideString; const permissionMask: Int64); stdcall;
    procedure UpdateRoleInfo(const oldRoleName: WideString; const roleName: WideString; const description: WideString; const permissionMask: Integer); stdcall;
    procedure UpdateRoleDefInfo(const oldRoleName: WideString; const roleName: WideString; const description: WideString; const permissionMask: Int64); stdcall;
    procedure RemoveRole(const roleName: WideString); stdcall;
    function  GetUserLoginFromEmail(const emailXml: emailXml): GetUserLoginFromEmailResult; stdcall;
    function  GetRolesAndPermissionsForCurrentUser: GetRolesAndPermissionsForCurrentUserResult; stdcall;
    function  GetRolesAndPermissionsForSite: GetRolesAndPermissionsForSiteResult; stdcall;
  end;
 
function GetUserGroupSoap(UseWSDL: Boolean=System.False; Addr: string=''; HTTPRIO: THTTPRIO = nil): UserGroupSoap;
 
 
implementation
 
function GetUserGroupSoap(UseWSDL: Boolean; Addr: string; HTTPRIO: THTTPRIO): UserGroupSoap;
const
  defWSDL = 'https://mawp.securespsites.com/_vti_bin/usergroup.asmx?WSDL';
  defURL  = 'https://mawp.securespsites.com/_vti_bin/usergroup.asmx';
  defSvc  = 'UserGroup';
  defPrt  = 'UserGroupSoap';
var
  RIO: THTTPRIO;
begin
  Result := nil;
  if (Addr = '') then
  begin
    if UseWSDL then
      Addr := defWSDL
    else
      Addr := defURL;
  end;
  if HTTPRIO = nil then
    RIO := THTTPRIO.Create(nil)
  else
    RIO := HTTPRIO;
  try
    Result := (RIO as UserGroupSoap);
    if UseWSDL then
    begin
      RIO.WSDLLocation := Addr;
      RIO.Service := defSvc;
      RIO.Port := defPrt;
    end else
      RIO.URL := Addr;
  finally
    if (Result = nil) and (HTTPRIO = nil) then
      RIO.Free;
  end;
end;
 
 
initialization
  InvRegistry.RegisterInterface(TypeInfo(UserGroupSoap), 'http://schemas.microsoft.com/sharepoint/soap/directory/', '');
  InvRegistry.RegisterDefaultSOAPAction(TypeInfo(UserGroupSoap), 'http://schemas.microsoft.com/sharepoint/soap/directory/%operationName%');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetUserCollectionFromSiteResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetUserCollectionFromSiteResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetUserCollectionFromWebResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetUserCollectionFromWebResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetAllUserCollectionFromWebResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetAllUserCollectionFromWebResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetUserCollectionFromGroupResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetUserCollectionFromGroupResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetUserCollectionFromRoleResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetUserCollectionFromRoleResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(userLoginNamesXml), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'userLoginNamesXml');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetUserCollectionResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetUserCollectionResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetUserInfoResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetUserInfoResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetGroupCollectionFromSiteResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetGroupCollectionFromSiteResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetGroupCollectionFromWebResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetGroupCollectionFromWebResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetGroupCollectionFromRoleResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetGroupCollectionFromRoleResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetGroupCollectionFromUserResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetGroupCollectionFromUserResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(groupNamesXml), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'groupNamesXml');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetGroupCollectionResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetGroupCollectionResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetGroupInfoResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetGroupInfoResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetRoleCollectionFromWebResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetRoleCollectionFromWebResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetRoleCollectionFromGroupResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetRoleCollectionFromGroupResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetRoleCollectionFromUserResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetRoleCollectionFromUserResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(roleNamesXml), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'roleNamesXml');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetRoleCollectionResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetRoleCollectionResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetRoleInfoResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetRoleInfoResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(emailXml), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'emailXml');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetUserLoginFromEmailResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetUserLoginFromEmailResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetRolesAndPermissionsForCurrentUserResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetRolesAndPermissionsForCurrentUserResult');
  RemClassRegistry.RegisterXSInfo(TypeInfo(GetRolesAndPermissionsForSiteResult), 'http://schemas.microsoft.com/sharepoint/soap/directory/', 'GetRolesAndPermissionsForSiteResult');
 
end.
 
 
 
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
 
 
unit Unit1;
 
interface
 
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, Buttons,
  shSharePointWS_UserGroup, ADODB, Grids, DBGrids, DB, ExtCtrls, RzPanel,
  IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient, IdHTTP, shSharePointWS_Authentication,
  PAIShare, DBClient, SOAPConn, InvokeRegistry, Rio, SOAPHTTPClient;
 
type
  TForm1 = class(TForm)
    Memo1: TMemo;
    DefaultRaizeListBoxWithColorsGBox: TRzGroupBox;
    BitBtn1: TBitBtn;
    HTTPRIO1: THTTPRIO;
    procedure BitBtn1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;
 
var
  Form1: TForm1;
 
implementation
 
{$R *.dfm}
 
 
procedure TForm1.BitBtn1Click(Sender: TObject);
var oMyGroup : UserGroupSoap;
    oAuth    : AuthenticationSoap;
    otmp     : TADOTable;
    oLoginResultInfo : LoginResult;
 
    cGroup    : widestring;
    cName     : widestring;
    cLogin    : widestring;
    cEmail    : widestring;
    cNotes    : widestring;
 
begin
   oMyGroup := GetUserGroupSoap(FALSE,
                                'https://mawp.securespsites.com/_vti_bin/usergroup.asmx',
                                HTTPRIO1);
   Memo1.Text := oMyGroup.GetUserCollectionFromGroup('MAWP Members')
   try
      //oMyGroup.AddUserToGroup('group','user','login','email','notes');
      //cGroup    := 'MAWP Visitors';
      //cName     := 'Ryan A. Ford';
      //cLogin    := 'SP\r.ford';
      //cEmail    := 'r.ford@myangelwithpaws.org';
      //cNotes    := '';
 
      //oMyGroup.AddUserToGroup(cGroup, cName, cLogin, cEmail, cNotes);
   except
        on E: Exception do
           begin
              MessageDlg('Error Encountered Adding User.' + #10#13 +
                         'Details: ' + #10#13 +
                         E.ClassType.ClassName + #10#13 +
                         E.Message,
                         mtError,[mbOk],0);
           end;
   end;
   try
      //oMyGroup.AddUserToGroup('group','user','login','email','notes');
      //
      //oMyGroup.AddUserToGroup('MAWP Visitors', 'Patrick', 'SP\Patrick', 'patrick@perrymansoftware.com', '');
   except
        on E: Exception do
           begin
              MessageDlg('Error Encountered Adding User.' + #10#13 +
                         'Details: ' + #10#13 +
                         E.ClassType.ClassName + #10#13 +
                         E.Message,
                         mtError,[mbOk],0);
           end;
   end;
 
   Memo1.Lines.SaveToFile('c:\tmpxmlinfo.xml');
end;
 
end.

Open in new window

0
Comment
Question by:Neranel
[X]
Welcome to Experts Exchange

Add your voice to the tech community where 5M+ people just like you are talking about what matters.

  • Help others & share knowledge
  • Earn cash & points
  • Learn & ask questions
  • 6
  • 4
10 Comments
 
LVL 51

Expert Comment

by:Ted Bouskill
ID: 24414375
Sharepoint uses Windows Authentication by default and all web services honor the security model set within it.  So, assuming Delphi is running on a Windows system, what ever domain account the Delphi application is running as will be the account used to access Sharepoint.

I'd recommend you use this tool to analyze and play with the web services: http://www.codeplex.com/WebserviceStudio  That way you can determine what the error is and if you are passing the write data and parameters.
0
 
LVL 5

Author Comment

by:Neranel
ID: 24414600
I am passing authentication through the HTTPRIO object.  This is a sharepoint site hosted over the internet using NTLM.  I can get responses back of lists of groups and users for the site and web as i do not have to pass data in to the web service.  So i know my issue is in passing the data to the web service, but can not figure out what is wrong.
0
 
LVL 51

Expert Comment

by:Ted Bouskill
ID: 24414628
If you are 100% sure it's not a permissions problem then use the tool I recommended to make sure you are using the web service correctly.
0
Industry Leaders: We Want Your Opinion!

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

 
LVL 5

Author Comment

by:Neranel
ID: 24414745
Ive been looking at the toold, and this is scary, its changing group memberships withoiut me ever giving credentials to login to the site.  However, I am passing the same values from my delphi applicaiton in the HTTPRIO object, but i get a SOAP error.
0
 
LVL 51

Expert Comment

by:Ted Bouskill
ID: 24415155
First off, if your personal account has 'Farm Admin' or 'Site Collection' privileges then yes when you run the tool it will relay your credentials and you might actually have those permissions.  If you want to test thoroughly create a domain test account that doesn't have the permissions and try the same tool.  You might get a completely different result.

As I said, that might be the issue with the Delphi application.  SOAP errors are very generic and can hide permission issues.
0
 
LVL 5

Author Comment

by:Neranel
ID: 24415246
My local user here has NOTHING to do with this sharepoint site.  This site is hosted elsewhere.
0
 
LVL 5

Author Comment

by:Neranel
ID: 24415259
again i can get results back for methods that do not require input, such as getusercollectionfromsite.  If i dont put credentials in my HTTPRIO object, i get a promt, but still get correct data back, if i do put the credentials in the object, it bypasses the prompt.
0
 
LVL 51

Expert Comment

by:Ted Bouskill
ID: 24415428
You have no control of the Sharepoint site?  That is a problem because you are at the mercy of how they have it configured.  So you don't have any access to the logs or settings for the Sharepoint site?
0
 
LVL 5

Accepted Solution

by:
Neranel earned 0 total points
ID: 24415790
RESOLVED.

Needed to add the following code to the imported WSDL.
InvRegistry.RegisterInvokeOptions(TypeInfo(UserGroupSoap), ioDocument);

Open in new window

0
 
LVL 5

Author Comment

by:Neranel
ID: 25004323
This is not neccessary in Delphi 2009 btw.
0

Featured Post

Simplifying Server Workload Migrations

This use case outlines the migration challenges that organizations face and how the Acronis AnyData Engine supports physical-to-physical (P2P), physical-to-virtual (P2V), virtual to physical (V2P), and cross-virtual (V2V) migration scenarios to address these challenges.

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

Summary In SharePoint 2010 it is easy to create custom color themes to jazz up a site. Theme colors can also be created in PowerPoint 2010 with a few clicks. But how do the chosen colors actually look in the SharePoint site? The attached PowerPoint…
I thought I'd write this up for anyone who has a request to create an anonymous whistle-blower-type submission form created using SharePoint 2010 (this would probably work the same for 2013). It's not 100% fool-proof but it's as close as you can get…
Monitoring a network: why having a policy is the best policy? Michael Kulchisky, MCSE, MCSA, MCP, VTSP, VSP, CCSP outlines the enormous benefits of having a policy-based approach when monitoring medium and large networks. Software utilized in this v…
This tutorial will teach you the special effect of super speed similar to the fictional character Wally West aka "The Flash" After Shake : http://www.videocopilot.net/presets/after_shake/ All lightning effects with instructions : http://www.mediaf…
Suggested Courses
Course of the Month3 days, 17 hours left to enroll

630 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question