[Previous] [Contents] [Next]

The IPersist Interface Family

At run time, most objects maintain certain information about their current state. The exact nature of the data and its format are defined by the author of each coclass, not by COM+. However, several standard persistence interfaces allow a client application to obtain and save the state of an object. An object declares its ability to serialize its state by implementing one or more of these interfaces. The persistence interfaces also enable a client to clone an object by loading its serialized state back into an object. Visual Basic, for example, invokes the persistence interfaces implemented by Microsoft ActiveX controls to obtain their current property values when a project is saved. When the project is reopened, the controls are instantiated and their property values are initialized from the persisted data. Microsoft Message Queue (MSMQ) uses the persistence interfaces to save an object's state for transmission in a message.

At the root of all the persistence interfaces is the IPersist interface itself, shown below in Interface Definition Language (IDL) notation:

interface IPersist : IUnknown
    HRESULT GetClassID([out] CLSID* pClassID);

This interface simply allows a client to obtain the class identifier (CLSID) of the object. Typically, a client application stores the CLSID of an object together with its state. In this way, the client application knows which object to activate when it reinstantiates the class. For stateless objects, the IPersist interface is sufficient because the client application needs to know only which class to activate. Most objects, however, need to store data in addition to their CLSID. Those objects can choose among several types of persistence interfaces that derive from the base IPersist interface. These are described in the table below.

Persistence Interface Description
IPersistStream Implemented by objects that save and load their state using a simple serial stream
IPersistStreamInit Like IPersistStream but with the addition of the InitNew method for initialization
IPersistMemory Like IPersistStreamInit except that the caller can provide a fixed-sized memory block instead of an arbitrarily expandable stream
IPersistStorage Implemented by objects that save and load their state using a storage object
IPersistFile Implemented by objects that save and load their state using a file
IPersistPropertyBag(2) Implemented by controls that save and load their state using a property bag
IPersistMoniker Implemented by objects that save and load their state using a moniker
IPersistHistory Implemented by objects running in Microsoft Internet Explorer that want to save and load their state when the user navigates among Web pages

IPersistStream is one of the fundamental interfaces in the IPersist family. Any object that implements the IPersistStream interface tells the world that it is happy to load and save its state to any stream object provided by the client. The four methods of IPersistStream are shown below in IDL notation:

interface IPersistStream : IPersist
    // Check the object for changes since it was last saved.
    HRESULT IsDirty(void);

    // Load the object's state from the stream.
    HRESULT Load([in, unique] IStream *pStm);

    // Save the object's state to the stream.
    HRESULT Save([in, unique] IStream *pStm, 
        [in] BOOL fClearDirty);

    // How many bytes are required to the save the 
    // object's state?
    HRESULT GetSizeMax([out] ULARGE_INTEGER *pcbSize);

Objects that implement IPersistStream expect to be initialized by the client via the IPersistStream::Load method before the client calls other methods of the IPersistStream interface. One variation of IPersistStream is the IPersistStreamInit interface, which is identical to IPersistStream except for the addition of the InitNew method. Clients can use this method to initialize the object to a default state instead of restoring a saved state via the IPersistStreamInit::Load method. The IPersistStreamInit interface is shown below in IDL notation; the InitNew method is shown in boldface.

interface IPersistStreamInit : IPersist
    HRESULT IsDirty(void);
    HRESULT Load([in] LPSTREAM pStm);
    HRESULT Save([in] LPSTREAM pStm, [in] BOOL fClearDirty);
    HRESULT GetSizeMax([out] ULARGE_INTEGER * pCbSize);

    // Initialize the object to its default state.
    HRESULT InitNew(void);

The IStream Interface

Notice that the first argument of both the Load and Save methods of the IPersistStream(Init) interface accepts an IStream pointer. IStream is a basic interface that encompasses the functionality of reading and writing to a sequential byte stream. Interestingly, IStream derives from the ISequentialStream1 interface, which defines the Read and Write methods, as shown here in IDL notation:

interface ISequentialStream : IUnknown
    // Reads a specified number of bytes from the stream object 
    // into memory, starting at the current seek pointer
    HRESULT Read(
        [out, size_is(cb), length_is(*pcbRead)] void *pv,
        [in] ULONG cb,
        [out] ULONG *pcbRead);

    // Writes a specified number of bytes into the stream 
    // object, starting at the current seek pointer
    HRESULT Write(
        [in, size_is(cb)] void const *pv,
        [in] ULONG cb,
        [out] ULONG *pcbWritten);

The IStream interface contains methods similar to those that can be executed on a file handle in that each stream has its own access rights and seek pointer. The IStream interface also provides support for a transacted mode and for restricting access to a range of bytes in the stream. The members of the IStream interface are described in the following table.

IStream Method Description
Seek Moves the seek pointer to a new location relative to the beginning of the stream, the end of the stream, or the current seek pointer
SetSize Changes the size of the stream object
CopyTo Copies a specified number of bytes from the current seek pointer in one stream to the current seek pointer in another stream
Commit Ensures that any changes made to a stream object open in transacted mode are reflected in the parent storage object
Revert Discards all changes that have been made to a transacted stream since the last IStream::Commit call
LockRegion Restricts access to a specified range of bytes in the stream
UnlockRegion Removes the access restriction on a range of bytes previously restricted using IStream::LockRegion
Stat Retrieves the STATSTG structure for this stream.*
Clone Creates a new stream object that references the same bytes as the original stream but provides a separate seek pointer to those bytes

* The STATSTG structure contains statistical information about an open storage, stream, or byte array object.

COM+ provides several standard implementations of the IStream interface, so you generally don't need to implement this interface yourself unless you require special functionality. The most popular implementation of this interface is provided by the structured storage service (described later in this chapter), which you can use to save a stream to a disk-based file. You can obtain an even more general implementation of the IStream interface by calling the CreateStreamOnHGlobal2 API function, which creates a stream object based on a block of global memory.

Persisting an Object

You persist an object by obtaining a pointer to its persistence interface using the QueryInterface method and then calling the appropriate Save method for that persistence interface. The Save method always requires a pointer to an object implemented by the client where the persistable object can store its data. These steps are illustrated in Figure 10-1, which shows a client directing the object to persist itself to a stream object.

Click to view at full size.

Figure 10-1. A client asks an object to store its state in a stream using the IPersistStream interface.

The code fragment below illustrates how a client application obtains a pointer to the IPersistStream interface implemented by a persistable object and then saves the object's state to a memory-based stream:

// Create the object.
IUnknown* pUnknown;
HRESULT hr = CoCreateInstance(CLSID_SomePersistentObject, NULL, 
    CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pUnknown);
    cout << "CoCreateInstance failed." << endl;

// Ask the object for IPersistStreamInit.
IPersistStreamInit* pPersistStreamInit;
hr = pUnknown->QueryInterface(IID_IPersistStreamInit, 
    cout << "IPersistStreamInit not supported." << endl;

// Create a memory-based stream.
IStream* pStream = 0;
hr = CreateStreamOnHGlobal(NULL, TRUE, &pStream);
    cout << "CreateStreamOnHGlobal failed." << endl;

// Initialize the object to its default state.
hr = pPersistStreamInit->InitNew();
    cout << "IPersistStreamInit::InitNew failed." << endl; 

// Save the object to the stream.
hr = pPersistStreamInit->Save(pStream, TRUE);
    cout << "IPersistStreamInit::Save failed." << endl;

// Release the object.

Once you have a stream containing the state of an object, you can create a clone of the object simply by instantiating a new object of the same coclass and passing the stream to the IPersistStream(Init)::Load method. Assuming that the object has properly implemented its persistence code, the newly created object should be indistinguishable from the original; the code fragment below shows how it's done:

// Rewind the stream to the beginning.
LARGE_INTEGER zero = { 0, 0 };
hr = pStream->Seek(zero, STREAM_SEEK_SET, NULL);
    cout << "IStream::Seek failed." << endl;

// Create a new object.
hr = CoCreateInstance(CLSID_SomePersistentObject, NULL, 
    CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pUnknown);
    cout << "CoCreateInstance failed." << endl;

// Ask the object for IPersistStreamInit.
hr = pUnknown->QueryInterface(IID_IPersistStreamInit, 
    cout << "IID_IPersistStreamInit not supported." << endl;

// Initialize the object from the stream.
hr = pPersistStreamInit->Load(pStream);
    cout << "IPersistStreamInit::Load failed." << endl;

// Release the stream.

Implementing a Persistable Object

As you can see from the code above, using the IPersist interfaces is not difficult. Implementing the IPersistStream(Init) interface in an object is also not difficult; it simply entails implementing the five methods of IPersistStream or the six methods of IPersistStreamInit. Let's begin with the IPersist::GetClassID method required by all implementations of the IPersist family of interfaces. The GetClassID method should simply return the CLSID of the object, as shown below:

    *pClassID = CLSID_InsideCOM;
    return S_OK;

The purpose of the IPersist::GetClassID method might not be clear initially, since this method has nothing to do with the persistence of a specific object. But its importance becomes obvious when you realize that once an object is persisted to a stream, nothing in that stream identifies the object itself. In other words, if you were to obtain a stream containing the persisted state of an unknown object, you wouldn't be able to re-create the original object from the stream. To address this problem, most client applications obtain the object's CLSID using the IPersist::GetClassID method and store that in the stream before the object's data.

The only problem with this approach is that client applications might choose to store an object's CLSID in a stream differently. For example, one client might convert the CLSID to a string before storing it in the stream, and another might store it as a hexadecimal byte array. This would lead to a situation in which a stream containing the persisted state of an object created by one client application could not be read by another client application. The WriteClassStm function automates the chore of saving an object's CLSID to a stream in a standard way; it ensures uniformity among all client applications that use it, as shown in the client-side code below:

// Get the CLSID of the persistable object.
CLSID clsid;
hr = pPersistStream->GetClassID(&clsid);

// Store the object's CLSID in the stream.
hr = WriteClassStm(pStream, clsid);

// Save the object's data to the stream.
hr = pPersistStream->Save(pStream, TRUE);

To re-create an object from a stream, you call the ReadClassStm function to obtain the CLSID stored by WriteClassStm. This allows the client application to instantiate the correct object via a call to CoCreateInstance(Ex) and then call IPersistStream(Init)::Load to initialize the object with the remaining data in the stream, as shown in the following code.

// Get the CLSID of the persistable object.
CLSID clsid;
hr = ReadClassStm(pStream, &clsid);

// Instantiate the appropriate object.
IPersistStream* pPersistStream;
hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, 
    IID_IPersistStream, (void**)&pPersistStream);

// Load the object's data from the stream.
hr = pPersistStream->Load(pStream);

The sequence of operations illustrated by the two preceding code fragments are so standard that two helper functions have been defined to automate the entire procedure: OleSaveToStream and OleLoadFromStream. OleSaveToStream calls IPersist::GetClassID to obtain the object's CLSID, calls WriteClassStm to save the object's CLSID to the stream, and then calls IPersistStream::Save to save the object's persistent data to the stream. OleLoadFromStream calls ReadClassStm to obtain the object's CLSID from the stream, calls CoCreateInstance to instantiate the object, and then calls IPersistStream::Load to initialize the object from the data in the stream. Thus all the code in the two fragments can be reduced to two function calls:

// Save the object to stream (with its CLSID).
hr = OleSaveToStream(pPersistStream, pStream);

// Sometime later, create a new object based on the 
// persisted stream.
IUnknown* pObject;
hr = OleLoadFromStream(pStream, IID_IUnknown, (void**)&pObject);

The IPersistStream(Init)::IsDirty method lets the client know whether the object has changed since it was last saved. A return value of S_OK indicates that the object is dirty; S_FALSE indicates that it is clean. A client application can use this information to decide whether the object needs to be persisted again using IPersistStream(Init)::Save. The code below is a simple implementation of the IPersistStream(Init)::IsDirty method:

HRESULT CInsideCOM::IsDirty(void)
        return S_OK;    // The object is dirty.
        return S_FALSE; // The object is clean.

The Save and Load methods are the heart of the IPersistStream(Init) interface. When the Save method is called, the object should use the ISequentialStream::Write method to store its current state in the IStream pointer provided as the first parameter to the Save method. The second parameter of the IPersistStream(Init)::Save method tells the object whether it should reset its dirty flag. If fClearDirty is TRUE, the dirty flag should be cleared, assuming that the object can successfully save its state to the stream; otherwise, the dirty flag should be left unchanged. The implementation of the IPersistStream::Save method for the InsideCOM coclass is shown below. Note that the ISequentialStream::Write method is used to save the values of the m_x and m_y variables obtained from the client during a prior call to the ISum::Sum method.

HRESULT CInsideCOM::Save(IStream *pStm, BOOL fClearDirty)
    // Save the data in the client's stream.
    ULONG written = 0;
    int data[2] = { m_x, m_y };
    HRESULT hr = pStm->Write(data, sizeof(data), &written);

    // Clear the dirty flag?
        m_dirty = FALSE;

    return S_OK;

The IPersistStream(Init)::Load method is responsible for reading data from the stream object provided in the first parameter and restoring the object's internal state to that specified by the stream data. The sample implementation of the Load method shown below uses the ISequentialStream::Read method to read data from the stream and restore the values of the m_x and m_y member variables:

HRESULT CInsideCOM::Load(IStream *pStm)
    // Read the data from the client's stream.
    ULONG read = 0;
    int data[2] = { 0, 0 };
    pStm->Read(data, sizeof(data), &read);

    // Restore the data back to our private variables.
    m_x = data[0];
    m_y = data[1];

    // Clear the dirty flag.
    m_dirty = FALSE;

    return S_OK;

GetSizeMax, the last method of the IPersistStream interface, tells the client how many bytes are needed in the stream to store the object's data. Since the InsideCOM coclass saves only two 32-bit integer values during the IPersistStream::Save operation, GetSizeMax returns 8.

    (*pcbSize).QuadPart = 8;
    return S_OK;

If you are implementing the IPersistStreamInit interface, you must implement the IPersistStreamInit::InitNew method in addition to the methods of IPersistStream. The InitNew method initializes the object to a default state; the client can call it instead of IPersistStreamInit::Load. If the client has already called Load, the InitNew method must return E_UNEXPECTED. In the case of the InsideCOM coclass, the InitNew method is used only to initialize the dirty flag:

HRESULT CInsideCOM::InitNew(void)
    // Initialize the dirty flag.
    m_dirty = FALSE;
    return S_OK;

Building Persistable Objects in Visual Basic

Persistence is supported by higher-level languages such as Visual Basic and Java. Visual Basic supports building persistable classes using the Persistable property of public class modules. If the Persistable property is set to 1 — Persistable, Visual Basic automatically provides an implementation of the IPersist, IPersistStream, IPersistStreamInit, and IPersistPropertyBag interfaces for the coclass. The IPersistPropertyBag interface is shown below in IDL notation:

interface IPersistPropertyBag : IPersist
    HRESULT InitNew(void);
    HRESULT Load([in] IPropertyBag* pPropBag, 
        [in] IErrorLog* pErrorLog);
    HRESULT Save([in] IPropertyBag* pPropBag, 
        [in] BOOL fClearDirty, 
        [in] BOOL fSaveAllProperties);

The IPersistPropertyBag interface is similar to the IPersistStreamInit interface in that it has the InitNew, Load, and Save methods.3 Notice, however, that the Load and Save methods of the IPersistPropertyBag interface do not access data using the IStream interface; instead, they expect to be provided with a property bag object that implements the IPropertyBag4 interface, which is shown below in IDL notation:

interface IPropertyBag : IUnknown
    // Called by the object to read a property from the storage 
    // provided by the client
    HRESULT Read([in] LPCOLESTR pszPropName, 
        [in, out] VARIANT* pVar, 
        [in] IErrorLog* pErrorLog);

    // Called by the object to write each property in turn to 
    // the storage provided by the client
    HRESULT Write([in] LPCOLESTR pszPropName, 
        [in] VARIANT* pVar);

Because it has Read and Write methods, the IPropertyBag interface is superficially similar to the ISequentialStream interface. However, instead of storing data as a sequential stream of bytes as the ISequentialStream interface does, the IPropertyBag interface was designed as an optimization for objects, such as ActiveX controls, that store primarily text-based property values. Most ActiveX controls implement the IPersistPropertyBag interface, and the Visual Basic development environment itself implements the IPropertyBag interface to collect and store the property values saved by controls into Visual Basic project files.

The IErrorLog interface is an abstraction of an "error log" for communicating detailed error information between a client and an object. It is used as part of the IPropertyBag and IPersistPropertyBag protocol to report any errors that occur using the IErrorLog::AddError method. The first parameter of the AddError method is the name of the property involved in the error, and the second parameter is an EXCEPINFO structure containing the error information. The IErrorLog interface is shown below in IDL notation:

    // Here's what happened...
    HRESULT AddError(
interface IErrorLog : IUnknown
        [in] LPCOLESTR pszPropName,
        [in] EXCEPINFO * pExcepInfo);

While this is all fascinating information, you're probably wondering why Visual Basic automatically implements the IPersistPropertyBag interface in every persistable class. The reason will become clear as we examine the three event procedures that are implemented in Visual Basic by every persistable class module—InitProperties, ReadProperties, and WriteProperties:

' Similar to IPersistPropertyBag::InitNew
Private Sub Class_InitProperties()
End Sub

' Similar to IPersistPropertyBag::Load
Private Sub Class_ReadProperties(PropBag As PropertyBag)
End Sub

' Similar to IPersistPropertyBag::Save
Private Sub Class_WriteProperties(PropBag As PropertyBag)
End Sub

Notice the one-to-one mapping between the three event procedures exposed in Visual Basic and the methods of the IPersistPropertyBag interface. The InitProperties event corresponds to the IPersistPropertyBag::InitNew or IPersistStreamInit::InitNew methods and is fired at the class module when the client calls one of those methods. The ReadProperties and WriteProperties events correspond to the IPersistPropertyBag::Load and IPersistPropertyBag::Save or IPersistStream(Init)::Load and IPersistStream(Init)::Save methods, respectively, and are fired when the client calls those methods. In fact, although Visual Basic also implements the IPersistStream(Init) interface for all persistable coclasses, the two additional methods of IPersistStream(Init) not addressed by IPersistPropertyBag, IPersistStream(Init)::IsDirty and IPersistStream(Init)::GetSizeMax, are not exposed in Visual Basic. Instead, in current versions of Visual Basic, the IsDirty method always returns S_OK5 and GetSizeMax returns E_NOTIMPL.

Notice that the ReadProperties and WriteProperties procedures both have a PropertyBag parameter, which you use when you actually save and load the state of an object in Visual Basic. The methods and properties of Visual Basic's PropertyBag class are shown below; notice the close correlation of these methods with the IPropertyBag interface described previously.

' A byte array representing the contents of the PropertyBag
Property Contents As Variant

' Returns a persisted value from a PropertyBag
' Similar to IPropertyBag::Read
Function ReadProperty(Name As String, [DefaultValue]) As Variant

' Writes a value to be persisted to a PropertyBag
' Similar to IPropertyBag::Write
Sub WriteProperty(Name As String, Value, [DefaultValue])

The code below is a fragment from a persistable class module written in Visual Basic that stores its state in a Visual Basic-provided PropertyBag object:

Private Sub Class_ReadProperties(PropBag As PropertyBag)
    m_x = PropBag.ReadProperty("x", 0)
    m_y = PropBag.ReadProperty("y", 0)
End Sub

Private Sub Class_WriteProperties(PropBag As PropertyBag)
    PropBag.WriteProperty "x", m_x
    PropBag.WriteProperty "y", m_y
End Sub

A client application written in C++ can obtain this data by calling IPersistStreamInit::Save or IPersistPropertyBag::Save. Since we've already seen how the IPersistStreamInit interface works, let's examine how the IPersistPropertyBag interface is used in C++. The code below invites a Visual Basic object to save its state in a property bag using the IPersistPropertyBag interface:

// Get the IPersistPropertyBag interface.
IPersistPropertyBag* pPersistPropertyBag;
HRESULT hr = pUnknown->QueryInterface(IID_IPersistPropertyBag, 
    cout << "IID_IPersistPropertyBag not supported." << endl;

// Initialize the object to its default state.
// This fires the InitProperties event in Visual Basic.
hr = pPersistPropertyBag->InitNew();
    cout << "IPersistPropertyBag::InitNew failed" << endl;

// Instantiate the property bag object.
IPropertyBag* pPropertyBag = new CPropertyBag();

// Tell the object to save its state to the property bag.
// This fires the WriteProperties event in Visual Basic.
hr = pPersistPropertyBag->Save(pPropertyBag, TRUE, TRUE);
    cout << "IPersistPropertyBag::Save failed" << endl;

The main difference between IPersistPropertyBag and IPersistStreamInit is that when you use IPersistStreamInit you create a stream object that implements the IStream interface simply by calling the CreateStreamOnHGlobal function, and when you use IPersistPropertyBag you must create a property bag object that implements the IPropertyBag interface. Unfortunately, there is no system-provided implementation of a property bag as there is with a stream; you're left to implement this interface yourself. Below is a simple implementation of the IPropertyBag::Write method that simply prints out the name of the property and the value saved by the persistent object:

// This method called by the persistable object during 
// IPersistPropertyBag::Save.
HRESULT CPropertyBag::Write(LPCOLESTR pszPropName, 
    VARIANT* pVar)
    // Just print the property name and value.
    wprintf(L"PropertyName = %s Value = %s\n", pszPropName, 
    return S_OK;

Controlling Persistence from Visual Basic

In addition to implementing several members of the IPersist interface family in Visual Basic, a Visual Basic application can also act as a client of these same persistence interfaces implemented by another object. When Visual Basic instantiates a coclass, it automatically calls QueryInterface to obtain the object's IPersistPropertyBag, IPersistStreamInit, or IPersistStream interface, in that order. The object is not required to support any of the persistence interfaces, but if it does, Visual Basic allows you to persist the object into a PropertyBag and to initialize a new object from the data saved in a PropertyBag. The Visual Basic code below shows how it's done; for demonstration purposes, it uses the C++ implementation of IPersistStreamInit described earlier in this chapter.

' InsideCOM supports IPersistStreamInit.
Dim ref As New InsideCOM

' The first time we touch the object, Visual Basic creates it
' and calls IPersistStreamInit::InitNew.
Print ref.SumPersist

' Internally, the object saves the values 3 and 4.
Print ref.Sum(3, 4)

' Create a PropertyBag object for our use.
Dim pb As New PropertyBag

' Save a string property named String.
pb.WriteProperty "String", "Guy"

' Persist the InsideCOM object into a property named COM+.
' Visual Basic calls IPersistStreamInit::GetClassID and 
' IPersistStreamInit::Save.
pb.WriteProperty "COM+", ref

' Display the contents of the PropertyBag (not a pretty sight).
Print pb.Contents

' Create another reference to the InsideCOM coclass.
Dim newref As InsideCOM

' Create a new InsideCOM object.
' Visual Basic calls IPersistStreamInit::Load with the data from the 
' PropertyBag.
Set newref = pb.ReadProperty("COM+")

' The new object prints 3 + 4 = 7, proving that the persistence 
' worked.
Print newref.SumPersist

Building Persistable Objects in Java

A class written in Java can support persistence by implementing the java.io.Serializable interface. This interface has no methods or properties—it serves only as a signal interface to indicate that the class supports persistence. For classes that implement the java.io.Serializable interface, the Microsoft Java Virtual Machine (VM) automatically supports the IPersist, IPersistStreamInit, and IPersistStorage interfaces. When a client application asks a Java class to save its state using one of the supported IPersist interfaces, the object's nonstatic and nontransient fields are automatically serialized. When the state of the object is later restored using the same IPersist interface, Java automatically restores the member variables saved previously. In this way, a Java component can participate in persistence operations with other COM+ components.

Java classes that require special control during the serialization and deserialization process can implement two methods, writeObject and readObject, with the exact signatures shown below:

// Called during IPersistStreamInit::Save
private void writeObject(java.io.ObjectOutputStream out) 
    throws java.io.IOException

// Called during IPersistStreamInit::Load
private void readObject(java.io.ObjectInputStream in) 
    throws java.io.IOException, ClassNotFoundException

Within the writeObject and readObject methods, you can use the methods of the java.io.ObjectOutputStream and java.io.ObjectInputStream classes to write and read data from the stream, respectively. You can invoke the default mechanism for serialization by calling the ObjectOutputStream.defaultWriteObject method in the writeObject method and the ObjectInputStream.defaultReadObject method in the readObject method, as shown in the code fragment below:

private void writeObject(java.io.ObjectOutputStream out) 
    throws java.io.IOException
    // Default write method

private void readObject(java.io.ObjectInputStream in) 
    throws java.io.IOException, ClassNotFoundException
    // Default read method