Want to protect your cyber security and still get fast solutions? Ask a secure question today.Go Premium

x
?
Solved

Get indexes from a ClientDataset

Posted on 2011-10-03
30
Medium Priority
?
1,513 Views
Last Modified: 2012-05-12
Hi all.

Here another little question for my beloved experts! This time I'm sure is something I'm missing about TClientDataset, so it would be simple to help me - I hope ;-)
 I wrote.. not, I'm trying to write a function to get all indexes form a ClientDataset and to store them in a StringList name/value pairs where the name is the field name that is the index and the value its property (ixPrimary, ixUnique and so on). That's the code:

function GetIndexes(cds: TClientDataSet): TStrings;
var
  I: Integer;
  FIdx: TIndexDef;
begin
  cds.GetIndexNames(dm.slIndexes);
  with cds.IndexDefs do
  begin
    for I := 0 to Count - 1 do
    begin
      FIdx := cds.IndexDefs.GetIndexForFields(Items[I].Name, True);
      if FIdx.Options = [ixPrimary] then
        dm.slIndexes.Add(IndexDefs.Items[I].Name+'=Primary')
      else if FIdx.Options = [ixUnique] then
        dm.slIndexes.Add(IndexDefs.Items[I].Name+'=Unique')
    end;
  end;
end;

Open in new window


This doesn't work if I rewrite it this way (for debugging purposes):
 
function GetIndexes(cds: TClientDataSet): TStrings;
var
  I: Integer;
  FIdx: TIndexDef;
begin
  ShowMessage(IntToStr(cds.IndexDefs.Count));
  with cds.IndexDefs do
  begin
    for I := 0 to Count - 1 do
      ShowMessage(IntToStr(I)+' = '+Items[I].Name);
  end;
  ShowMessage(dm.slIndexes.Text);
end;

Open in new window


I get that the count is 0 and the StringList is empty: but the ClientDataset I pass to this function has three (3) indexes set at design time in the IndexDefs editor.

Can someone explain where and why I'm wrong?

As usual, I'll be very grateful for any help or siggestion.

Cheers
0
Comment
Question by:Marco Gasi
  • 13
  • 8
  • 6
  • +1
29 Comments
 
LVL 31

Author Comment

by:Marco Gasi
ID: 36907996
This is strange: no comments and are  past more than 14 hours... Nor I receive the usual email from EE "Your Question has yet to receive a comment": this seems to be an invisible question :-)
0
 
LVL 31

Author Comment

by:Marco Gasi
ID: 36909778
@aikimark

Thank you for the message: I was wondering to not had received the EE email about the now answered question yet - and even to have not received any comment: I thought it would be a simple question to answer to :-)

Cheers
0
 
LVL 46

Expert Comment

by:aikimark
ID: 36909901
I, too, would have expected someone to have addressed your question by now.
0
Independent Software Vendors: 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 21

Expert Comment

by:developmentguru
ID: 36910362
It might help to have the 4 index definitions and the associated field defs.  This would enable us to reproduce your situation.
0
 
LVL 21

Expert Comment

by:developmentguru
ID: 36910391
One other thing I should point out...

You should be using the name of the index instead of using a field name.  An index can have more than one field in it's definition.  For that matter each field that is part of an index could have different properties (If I read you right then one field in the index could be descending).  If you want to assume that there is only one field and only the first set of properties (for the one field) would be used... it would simplify the example though.
0
 
LVL 31

Author Comment

by:Marco Gasi
ID: 36910532
@developmentguru

Many thanks for your reply. I post here a schema of my table. Effectively, I'm assuming to have only one field per Index, but I'll have to consider that thay could be many more. For the moment, we can work assuming one field per index. But my function doesn't contain any about this and I don't understand why IndexDefs.Count result 0.
Keep in mind that I open dataset programmatically... Maybe to know I'm trying to do helps you to help me :-)
I'm writing a little utility to migrate tables from a ClientDataset to MySql. Actually, my program works fine and produces one or more dump files one can import in MySql using any mysql tool (phpMyAdmin etc): table structure is created correctly (in the limits of the correspondance between respective field types) and data are imported also.

Now I would add a basic index importation and here I stopped since I can't get rid of this function.

Hope to have been clear.


Index Name    Index Field Name      Field Type   Options

IndexProdNo       ProdNo                    ftString        []
IndexProdID        ProdID                    ftString        []
IndexRicNo          RicNo                       ftString        []


Cheers
0
 
LVL 8

Expert Comment

by:lomo74
ID: 36910662
count is zero... mmmh can you post the code of the DFM that defines the clientdataset?

besides this, this line sounds strange:

>> FIdx := cds.IndexDefs.GetIndexForFields(Items[ I ].Name, True);

lat's say you have an index named 'IDX1' which contains fields 'STRING1,INT1'
so the call resolves to
FIdx := cds.IndexDefs.GetIndexForFields('IDX1', True);
which returns nothing, right? because the function GetIndexForFields is supposed to return the index whose fields start with the ones supplied as the first argument.
the line should be simply:
FIdx := cds.IndexDefs.Items[ I ];

then, another thing
>> if FIdx.Options = [ixPrimary] then
FIdx.Options is a set so it can contain many values. eg. it can be [ixUnique, ixDescending].
use this syntax instead:

if ixPrimary in FIdx.Options then

this tests for the presence of a value in a set rather than testing the set against the value for equality.
0
 
LVL 31

Author Comment

by:Marco Gasi
ID: 36910873
Hi, Lorenzo.

How do you do? I had plan to contact you but I still didn't it. Well, you find in the code snippet the code of prodotti.cds. For your other notes, what I am stupid! if Options = ixPrimary!

Bye
var
  cdsProdotti: TClientDataSet;
  cdsProdottiProdNo: TStringField;
  cdsProdottiProdID: TStringField;
  cdsProdottiCategoria: TStringField;
  cdsProdottiRicNo: TStringField;

  cdsProdotti := TClientDataSet.Create(Self);
  cdsProdottiProdNo := TStringField.Create(Self);
  cdsProdottiProdID := TStringField.Create(Self);
  cdsProdottiCategoria := TStringField.Create(Self);
  cdsProdottiRicNo := TStringField.Create(Self);
  with cdsProdotti do
  begin
    Name := 'cdsProdotti';
    Active := True;
    Aggregates := <>;
    FileName := 'D:\MyPrograms\Gelateria\prodotti.cds';
    with FieldDefs.Add do begin 
      Name := 'ProdNo';
      DataType := ftString;
      Size := 20;
    end;
    with FieldDefs.Add do begin 
      Name := 'ProdID';
      DataType := ftString;
      Size := 20;
    end;
    with FieldDefs.Add do begin 
      Name := 'Categoria';
      DataType := ftString;
      Size := 20;
    end;
    with FieldDefs.Add do begin 
      Name := 'RicNo';
      DataType := ftString;
      Size := 20;
    end;
    with IndexDefs.Add do begin 
      Name := 'IndexProdNo';
      Fields := 'ProdNo';
    end;
    with IndexDefs.Add do begin 
      Name := 'IndexProdID';
      Fields := 'ProdID';
    end;
    with IndexDefs.Add do begin 
      Name := 'IndexRicNo';
      Fields := 'RicNo';
    end;
    IndexName := 'IndexRicNo';
    Params := <>;
    StoreDefs := True;
    AfterDelete := cdsProdottiAfterDelete;
//  Data := // please assign
  end;
  with cdsProdottiProdNo do
  begin
    Name := 'cdsProdottiProdNo';
  end;
  with cdsProdottiProdID do
  begin
    Name := 'cdsProdottiProdID';
  end;
  with cdsProdottiCategoria do
  begin
    Name := 'cdsProdottiCategoria';
  end;
  with cdsProdottiRicNo do
  begin
    Name := 'cdsProdottiRicNo';
  end;

Open in new window

0
 
LVL 21

Expert Comment

by:developmentguru
ID: 36912989
I took your code and neatened it up a bit and put it in the OnClick of a btnCreateCDS.  I added a separate button btnGetIndexes as well.  Clicking Get Indexes will list the fields as you requested on the list box lbResults.  While I did retrieve the value of the options for each index, I left the translation to string for you to do as this is dependent on the version of Delphi in use (use the type info unit).

To use this, create a blank form and add the 3 controls to it.  I named the form file frmMain.pas and named the form as fMain.  Once you get these basics set up you should be able to paste my code over your unit and run it.  Click the Create CDS button, then the Get Indexes button.

I think you had several issues in how the CDS was being created.  What I have here works, see if it helps you figure out what went wrong... I find that unless I let the CDS create it's own fields and indexes with the CreateDataset method, it often does not work correctly.
---PAS---

unit frmMain;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, DB, DBClient, StdCtrls, TypInfo;

type
  TfMain = class(TForm)
    btnCreateCDS: TButton;
    ClientDataSet1: TClientDataSet;
    btnGetIndexes: TButton;
    lbResults: TListBox;
    procedure btnCreateCDSClick(Sender: TObject);
    procedure btnGetIndexesClick(Sender: TObject);
  private
    { Private declarations }
    cdsProdotti: TClientDataSet;
    cdsProdottiProdNo: TStringField;
    cdsProdottiProdID: TStringField;
    cdsProdottiCategoria: TStringField;
    cdsProdottiRicNo: TStringField;
  public
    { Public declarations }
  end;

var
  fMain: TfMain;

implementation

{$R *.dfm}

procedure TfMain.btnCreateCDSClick(Sender: TObject);
begin
  cdsProdotti := TClientDataSet.Create(Self);

  with cdsProdotti do
    begin
      Name := 'cdsProdotti';

      FieldDefs.Add('ProdNo', ftString, 20);
      FieldDefs.Add('ProdID', ftString, 20);
      FieldDefs.Add('Categoria', ftString, 20);
      FieldDefs.Add('RicNo', ftString, 20);

      IndexDefs.Add('IndexProdNo', 'ProdNo', []);
      IndexDefs.Add('IndexProdID', 'ProdID', []);
      IndexDefs.Add('IndexRicNo', 'RicNo', []);

      CreateDataSet; //automatically makes it active

      //The fields should be created by the CDS and looked up
      cdsProdottiProdNo := FieldByName('ProdNo') as TStringField;
      cdsProdottiProdID := FieldByName('ProdID') as TStringField;
      cdsProdottiCategoria := FieldByName('Categoria') as TStringField;
      cdsProdottiRicNo := FieldByName('RicNo') as TStringField;

      //Set the index AFTER the dataset is created
      IndexName := 'IndexRicNo';

      StoreDefs := True;
      FileName := 'D:\MyPrograms\Gelateria\prodotti.cds';
    end;

  cdsProdottiProdNo.Name := 'cdsProdottiProdNo';
  cdsProdottiProdID.Name := 'cdsProdottiProdID';
  cdsProdottiCategoria.Name := 'cdsProdottiCategoria';
  cdsProdottiRicNo.Name := 'cdsProdottiRicNo';
end;

procedure TfMain.btnGetIndexesClick(Sender: TObject);
var
  I : Integer;
  Index : TIndexDef;
  IndexName : string;
  IndexFieldNames : string;
  IndexOptions : TIndexOptions;
  TextOptions : string;

begin
  for I := 0 to cdsProdotti.IndexDefs.Count - 1 do
    begin
      Index := cdsProdotti.IndexDefs[I];
      IndexName := Index.Name;
      IndexFieldNames := Index.FieldExpression;
      IndexOptions := Index.Options;

      //I will leave the conversion of the types to string as this is not
      //difficult but could be Delphi version dependant
      lbResults.Items.Add(IndexFieldNames + '=' + '[]');
    end;
end;

end.

Open in new window

0
 
LVL 21

Expert Comment

by:developmentguru
ID: 36912996
As a P.S.

When running this program it errors when you exit because it is unable to find the file it was told to save the definitions too... I left it and assumed you would handle the rest.
0
 
LVL 31

Author Comment

by:Marco Gasi
ID: 36913230
I'll try your code tomorrow, but I don't have any reason to doubt about what you say. So it seems that CreateDataset statement is foundamental: i had suspected this but I found that using this statement raised an error like 'Can't perform this action on an open dataset' if I create it after a call to cds.Open. Otherwise, if I call CreateDataset immediately after assigning the file to FileName property, than the error is that no field has been found.
It seems like one has to read the cds file, create dataset manually as you did but on file content basis and then maybe it works the procedure to get indexes. Now the problem could be: how to read a cds file? if the file is xml, one can parse it, but a binary file?

It seems like even this time I eneterd in a 'highway to hell' ;-)

What do you think about?
0
 
LVL 46

Expert Comment

by:aikimark
ID: 36913512
Unfortunately, the (binary) CDS format is proprietary and its format is unpublished, as is the TClientDataset source code.
0
 
LVL 21

Expert Comment

by:developmentguru
ID: 36914783
I had an issue recently... I tried to manually create a client dataset to use for a RAVE report I was working on.  I could not get RAVE to see this dataset's field definitions.  In order to get it to work I had to manually save the dataset I created to a stream and read it into a new dataset.  This allowed the client dataset to create the fields and that was enough to get it to work.  If needed, I could dig up that code and see if it will help you.

Let me know.
0
 
LVL 31

Author Comment

by:Marco Gasi
ID: 36914803
hmm, this sounds interesting, developmentguru. Yes, I would like to see your code: it seems to be the only way it could work, if it can. I'll can test later: this morning I have too much things to do:-(

Thank you
0
 
LVL 21

Expert Comment

by:developmentguru
ID: 36917665
I thought about this some more last night too... If you are loading a CDS to produce the new definition then you should be able to create and load it without making any calls to define fields.  In my program I did something like this...

CDS := TClientDataset.Create(MyDataModule);

if FileExists(MyCDS) then
  CDS.Load(MyCDS)
else
  begin
    //create fields and indexes
  end;

And, of course, save the file on the way out of the program.
0
 
LVL 31

Author Comment

by:Marco Gasi
ID: 36917918
I did so:
procedure TfrmMain.actOpenExecute(Sender: TObject);
begin
  dlgOpen1.Options := dlgOpen1.Options - [ofAllowMultiSelect];
  if dlgOpen1.Execute then
  try
    dm.dynCds := TClientDataSet.Create(dm);
    dm.dynCds.LoadFromFile(dlgOpen1.FileName);
    dm.dynCds.SaveToFile(ExtractFilePath(Application.ExeName)+'mycds.cds');
    dm.dynCds.FileName := ExtractFilePath(Application.ExeName)+'mycds.cds';

{this is my original code}
//    dm.cds.FileName := dlgOpen1.FileName;
//    dm.cds.Open;
//    dm.cds.CreateDataSet;
//    dm.cds.Active := True;
    dm.TblName := LeftStr(ExtractFileName(dm.cds.FileName), Length(ExtractFileName(dm.cds.FileName))-4);
    //this is to fill aStringGrid with table structure elements 
    CreateElements(Sender);
  except
    Application.MessageBox('Something went wrong opening file: ensure that is a dataset file in the correct format and retry, please.',
      'Application.Title', MB_OK + MB_ICONSTOP + MB_TOPMOST);
  end;
end;

Open in new window


CreateElements calls GetIndexes passing it the dynCds but nothing has changed: IndexDefs is 0...
0
 
LVL 21

Expert Comment

by:developmentguru
ID: 36919748
Here was the "magic" line for me.  I had created a client dataset in memory, defining all of the fields.  When I passed this to RAVE it would not recognize the fields in order to allow for visual layout.  I placed a TClientDataset on my data module and used the following line to get the data into it.

//data module data assigned from locally created data
DataMod.cdsCalendarPrint.XMLData := cdsCalendarPrint.XMLData;

This did not have anything to do with indexes, but the idea of letting the client dataset engine do the heavy lifting might get you moving in the right direction.
0
 
LVL 8

Expert Comment

by:lomo74
ID: 36922638
Marco - go one step back.
Why are you creating the ClientDataset and all of its fields and indexes by code instead of creating them on the form and editing them with the object inspector?
I suspect that all of the troubles you are experiencing are due to the order you create the fields / indexes, the way you are assigning properties and connecting the objects, and so on.
I'm not saying you cannot do that; but are you absolutely sure that creating a dataset by code that way (post #36910873), you end up with the same object you'd have obtained by creating it with the object inspector?
I'd not make a bet on that. For example (off the top of my head): you set StoreDefs := True after you've created fields and indexes definitions.
I think all the other actions you are undertaking are only attempts to fix an object that is corrupted since the beginning.
You have to fix the problem starting with the object creation IMHO. More details to come, as soon as I carry out some tests to prove my theory.
0
 
LVL 31

Author Comment

by:Marco Gasi
ID: 36922715
Hi, Lorenzo. In the post #36910873 I posted the dfm code as you required. Let me explain again what I'm trying to do (ID:36910532)

I'm bulding a program to allow to any user to convert one or more ClientDataset to one or more MySql dump files so he can migrate a whole database in its web server and use with php or any other server-side language (or even with Delphi itself but having a database wich can use in multiple ways with multiple langauages).

So I can't create dataset at design time: my program has a button 'Open' which allows user to choose a cds/xml file which contains a tablestructure and data and creates the dump file: this works. Now I wish to get some indexes: Obviously the final dump file will can be edited by user to made that adjustement my program can't do automatically, but my program will do a lot of work for the user which shall do only little edits (this is my intention...)

But, ven if table structure and data is created correctly, I can't get indexes and I don't understand why.

'See' you later.
0
 
LVL 8

Expert Comment

by:lomo74
ID: 36922832
Ciao, Marco.

>> In the post #36910873 I posted the dfm code as you required.

No. That is NOT dfm. That's pure pascal code.
Ok. The reasons why you're creating objects "at runtime" instead of "at design time" are clear.
So... two questions.
(1) if your program is a "general purpose clientdataset dumper", what the hell is "cdsProdotti" (translated for non italians: cdsItems)?
This is a particular dataset, with fields and so on. So: are you experiencing problems with cdsProdotti (which is created from pascal code), or with generic datasets restored from .cds/.xml files?
(2) if the latter applies, then it would be useful to know how that .cds/.xml file was created. We're assuming that the .cds/.xml file you're reading contains all data/datadefs/indexdefs and that your program isn't reading indexdefs properly. IMHO we have no guarantee that the .cds/.xml file contains what we expect, unless we exactly know how it was created.

Can you post the .cds file somewhere, and maybe the code that produced that .cds?
0
 
LVL 31

Author Comment

by:Marco Gasi
ID: 36923640
Ciao, Lorenzo.

0) How can I give you dfm code for cds? How can I get it? I don't know...

1) Forget cdsProdotti. My actual code is the one posted in my original question: I placed a datasoiurce and a clientdataset in a datamodule,  giving to clientdatasert tha name 'cds' at dasign time. At runtime I give to it its FileName and I open it. So cdsProdotti, cdsItems and so on are not used by me in this code. cdsProdotti is the original name I called a ClientDataset in another application I wrote for my Ice-Crémerie. This project born by my need to export that database in MySql to manage that data via Web. But whan I start a project I try to make it professional and more flexible as I can, so I'm here.

2) cdsProdotti was created at design-time by me using FiedlDefs editor and ObjectInspector

3) You (all, if interested in) can download .cds file here (but don't forget to say me how can I get dfm for cds): http://www.delphicoding.com/prodotti.cds

There is no secrets in this table, the secrets are in ricette.cds (recipes.cds for non-italian) ;-)

Thanks
0
 
LVL 8

Expert Comment

by:lomo74
ID: 36923815
0) right click / view as text - PAY ATTENTION: do not edit this source manually. Just copy-paste to EE. Once done, right click / view as form.

1) ok. Please, Marco; consider that we know about your problem as much as you post on EE. If a line of code is irrelevant, leave it off the discussion or we will have a bad understanding of what's your real problem.

-- summing it up --
a) you have a ClientDataSet on a form
b) you're trying to restore its contents (data and structure) from a .cds file
c) then, you want to "dump" this object to some other language (let's say mysql)

your problem is at (c), but it could originate at (b), right?
let me examine your .cds
0
 
LVL 21

Expert Comment

by:developmentguru
ID: 36924399
I believe the source code for creating the CDS was given in response to my questions and not indicative of how the CDS is actually created (in response to my post 36910362).  I hope that this is true, otherwise the handling of the CDS would easily cause the problems you are seeing.  I am away from my desk this morning so I will need to look at the CDS later in the day.

One thing that I noticed though... I have never used the CDS property StoreDefs by setting it to true.  I have always just saved, and loaded, from a .CDS file.  I wonder if that is what saved me much of this difficulty.

I will let you know what I find once I am back to my system (with Delphi on it).
0
 
LVL 31

Author Comment

by:Marco Gasi
ID: 36924491
@developmentguru
I believe the source code for creating the CDS was given in response to my questions and not indicative of how the CDS is actually created (in response to my post 36910362).  I hope that this is true, otherwise the handling of the CDS would easily cause the problems you are seeing.

The code I have posted is what I get using the CnPack function Components to code: I created that cds manually using FieldDefs, FieldEditor and Object Inspector: you're saying it is bad structured? Or the difficult is in StoreDefs? I'm not an expert about database or ClientDataset (or about anything else) so perhaps I made some error creating that cds but I don't figure out what type of error you are intending.

But don't worry. I'll wait for your comments when you'll have been come back to your desk.

Thanks
0
 
LVL 8

Accepted Solution

by:
lomo74 earned 2000 total points
ID: 36938299
Ciao, Marco.
A simple test I made shows that what you're trying to achieve is almost impossible.
1) Create a clientdataset, add some fields and indexes;
2) Save the dataset to a file (cds or xml, it does not matter); you can do this by right-clicking on the component and choosing one of the "Save to..." commands;
3) Create another clientdataset; restore it from the previously created file (right-click on the component, then "Load from MyBase table...";
4) Look at the new clientdataset definition: it contains fields (and maybe data) but no indexes.
This means that the clientdataset saves fields defs and data to the file, but it does not save index defs.
If you save the clientdataset in xml format and look at the xml code with a notepad, you can see by yourself that there is no index defs at all.
So: you can't restore a clientdataset's full definition (including indexes) from a .cds or .xml file; you can only restore fields and data from there.
For index defs, you need access to the component itself, or to the DFM that defined that component (if any).
Cheers - Lorenzo -
0
 
LVL 31

Author Comment

by:Marco Gasi
ID: 36938480
Ciao, Lorenzo.
Effective as usual, eh? Well, so it seems what I thought to be easy is impossible: the important thing is to have clear ideas ;-)

Thank you very mutch: I'll leave my user edit indexes manually by himself: all in all tha more boring work in such kind of task has been yet made.

On to the next, Lorenzo.
0
 
LVL 31

Author Closing Comment

by:Marco Gasi
ID: 36938486
Thanks Lorenzo, and thanks to you also, developmentguru, for having tried to help me.

Cheers
0
 
LVL 8

Expert Comment

by:lomo74
ID: 36946875
Marco; thank you.
A premise: they're your points, and you can do whatever you want with them.
But I feel it would be more fair if you splitted the points among me and developmentguru, who gave an important contribution to the discussion.
Cheers - Lorenzo -
0
 
LVL 31

Author Comment

by:Marco Gasi
ID: 36947107
Do you think? Effectively, I read two times the whole thread to decide. I wished to award points to developmentguru also to appreciate his efforts, but he always seems to think, as me, that task was possible and suggest some hypothesis about possible solutions, whereas you did some test and demonstrated that task simply was impossible.
The reason I decided to not award points to developmentguru is that none of his comments is near to the comment ultimately accepted as solution. I always try to give points to everyone, but this time I thought it wasn't in harmony with EE philosophy. I don't want to be ungrateful, developmentguru, but if I would received points every time I tried to help (I'm talking about PHP zone), I now woiuld be a genius!

Cheers - Marco
0

Featured Post

VIDEO: THE CONCERTO CLOUD FOR HEALTHCARE

Modern healthcare requires a modern cloud. View this brief video to understand how the Concerto Cloud for Healthcare can help your organization.

Question has a verified solution.

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

The uses clause is one of those things that just tends to grow and grow. Most of the time this is in the main form, as it's from this form that all others are called. If you have a big application (including many forms), the uses clause in the in…
Have you ever had your Delphi form/application just hanging while waiting for data to load? This is the article to read if you want to learn some things about adding threads for data loading in the background. First, I'll setup a general applica…
This video shows how to quickly and easily deploy an email signature for all users in Office 365 and prevent it from being added to replies and forwards. (the resulting signature is applied on the server level in Exchange Online) The email signat…
In a question here at Experts Exchange (https://www.experts-exchange.com/questions/29062564/Adobe-acrobat-reader-DC.html), a member asked how to create a signature in Adobe Acrobat Reader DC (the free Reader product, not the paid, full Acrobat produ…

572 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