Click here to Skip to main content
15,889,034 members
Articles / Programming Languages / C#

Magnetic Tape Data Storage. Part 1: Tape Drive - IO Commands

Rate me:
Please Sign up or sign in to vote.
4.63/5 (7 votes)
12 Sep 2006CPOL3 min read 114.9K   1.1K   30   48
This article describes the simple way to implement Read/Write operation on tape device

Introduction

This article describes the simple way to implement Read/Write operation on tape device. Attached zip file contains TapeOperator class that exposes Load, Read, Write and Close methods and BlockSize property. All these can be used as part of backup utility.

Step 1 - Configuration

Congratulations, you've bought a tape device and connected it to your PC. Now you can implement simple backup utility in C#. Actually there are three ways to work with your tape:

  1. You can operate it via driver
  2. You can work with it via some management software (if it's provided by the device manufacturer)
  3. And the last, simplest way to do it is to operate it via OS handle. This is our case; I'll show to you how you can operate it via OS handle

It is well known that each external device is interpreted by OS as a file, it's true for tape devices as well. The only thing that you have to find out before we start is the appropriate device file name. Open the device manager Window of your PC and you'll see your tape device under the Tape drives node (see picture 1 below).

Sample screenshot

Picture 1

Perform mouse right click on your tape device node and choose Tape Symbolic Name tab. In this tab you can see tape symbolic name, Tape0 in our example (see Picture 2). This name can be used to create a handle for your device.

Sample screenshot

Picture 2

So the appropriate file name for your device is @file://./Tape0.

Step 2 - Implementation

TapeOperator methods description:

  1. Load: gets as parameter name of your tape device ( "\\.\Tape0"). It creates a handle to the device by CreateFile and performs PrepareTape which executes certain procedures to prepare the given tape for I/O, both methods are part of Win32 API and therefore INTEROP is used to invoke it. Now you have the OS handle for your tape device, and it can be interpreted like a regular file handle, I mean you can open System.IO.FileStream. The only difference is that you can't seek opened FileStream, instead use SetTapePosition Win32 API (see read and write operations).
    C#
    /// <summary>
    /// Loads tape with given name. 
    /// </summary>
    
    public void Load( string tapeName )
    {
        // Try to open the file.
        
        m_handleValue = CreateFile(
            tapeName,
            GENERIC_READ | GENERIC_WRITE,
            0,
            IntPtr.Zero,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_ARCHIVE | FILE_FLAG_BACKUP_SEMANTICS,
            IntPtr.Zero
            );
        if ( m_handleValue.IsInvalid )
        {
            throw new TapeOperatorWin32Exception(
                "CreateFile", Marshal.GetLastWin32Error() );
        }
        // Load the tape
        
        int result = PrepareTape(
            m_handleValue,
            TAPE_LOAD,
            TRUE
            );
        if ( result != NO_ERROR )
        {
            throw new TapeOperatorWin32Exception(
                 "PrepareTape", Marshal.GetLastWin32Error() );
        }
        m_stream = new FileStream(
            m_handleValue,
            FileAccess.ReadWrite,
            65536,
            false
            );
    }
  2. Write: gets the start position and byte array to write. As it was shown, now you can access your tape device via the opened FileStream. Just invoke FileStrem.Write and FileSream.Flush methods. One thing you have to remember, all I/O operations on tape must be done in multiplies of block size. Each device has min, default and max block sizes (in my example I've used default block size - 65536), these values can be fetched by GetTapeParameters method (Win32 API), for more information, see BlockSize property implementation.
    C#
    /// <summary>
    /// Writes to the tape given stream starting from given position
    /// </summary>
    /// <param name="startPos"></param>
    /// <param name="stream"></param>
    
    public void Write( long startPos, byte[] stream )
    {
        // Get number of blocks that will be needed to perform write
        
        uint numberOfBlocks = GetBlocksNumber( stream.Length );
        // Updates tape's current position
        
        SetTapePosition( startPos );
        
        byte[] arrayToWrite = new byte[ numberOfBlocks * BlockSize ];
        Array.Copy( stream, arrayToWrite, stream.Length );
        // Write data to the device
        
        m_stream.Write( stream, 0, stream.Length );
        m_stream.Flush();
    }
  3. Read: gets the start positions and number of bytes to read.

    *By the way, pay attention on SetTapePosition (Win32 API) last parameter, it's type is BOOL in WIN32 definition. Don't pass .NET Boolean type for BOOL and BOOLEAN of WIN32. The size of .NET Boolean is 2 bytes, the size of BOOL is 4 bytes and size of BOOLEAN is 1 byte, therefore each Win32 API method with BOOL or BO<code>OLEAN parameter will return error for .NET Boolean pass attempt.

    C#
    /// <summary>
    /// Read one logical block from tape 
    /// starting on the given position
    /// </summary>
    /// <returns></returns>
    
    public byte[] Read( long startPosition )
    {
        byte[] buffer = new byte[ BlockSize ];
        SetTapePosition( startPosition );
        
        m_stream.Read( buffer, 0, buffer.Length );
        m_stream.Flush();
        return buffer;
    }
  4. Close: closes the stream and releases unmanaged resources. You can add to this code call to LoadTape with TAPE_UNLOAD parameter to eject tape from drive.
    C#
    /// <summary>
    /// Closes handler of the current tape
    /// </summary>
    
    public void Close()
    {
        if ( m_handleValue != null &&
            !m_handleValue.IsInvalid &&
            !m_handleValue.IsClosed )
        {
            m_handleValue.Close();
        }
    }
  5. BlockSize: This property returns the tape's default block size by invocation of GetTapeParameters method. One of the GetTapeParameters is a reference to the structure that is filled by Win32 method. So Marshal class is used to allocate unmanaged memory and copy between managed and unmanaged structures. See the source code below:
    C#
    /// <summary>
    /// Returns default block size for current
    /// device
    /// </summary>
    public uint BlockSize
    {
        get
        {
            IntPtr ptr = IntPtr.Zero;
            try
            {
                if ( !m_driveInfo.HasValue )
                {
                    m_driveInfo = new DriveInfo();
                    // Allocate unmanaged memory
                    
                    int size = Marshal.SizeOf( m_driveInfo );
                    ptr = Marshal.AllocHGlobal( size );
                    Marshal.StructureToPtr(
                        m_driveInfo,
                        ptr,
                        false
                    );
                    
                    int result = 0;
                    if ( ( result = GetTapeParameters(
                        m_handleValue,
                        DRIVE_PARAMS,
                        ref size,
                        ptr ) ) != NO_ERROR )
                    {
                        throw new TapeOperatorWin32Exception(
                            "GetTapeParameters", 
                            Marshal.GetLastWin32Error() );    
                    }
                    // Get managed media Info
                    
                    m_driveInfo = ( DriveInfo )
                        Marshal.PtrToStructure
                ( ptr, typeof( DriveInfo ) );
                }
                
                return m_driveInfo.Value.DefaultBlockSize;
            }
            finally
            {
                if ( ptr != IntPtr.Zero )
                {
                    Marshal.FreeHGlobal( ptr );
                }
            }
        }
  6. See attached files for more information about defined constants, private variables, private methods and PINVOKE declarations.
  7. Good luck!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Retired
Israel Israel
Name: Statz Dima
Fields of interest: software

Comments and Discussions

 
GeneralRe: USB TAPE Pin
Dima Statz26-Sep-10 3:34
Dima Statz26-Sep-10 3:34 
QuestionWhat about VBScript ? Pin
stephane.reiniche9-Mar-07 0:00
stephane.reiniche9-Mar-07 0:00 
AnswerRe: What about VBScript ? Pin
Dima Statz10-Mar-07 23:38
Dima Statz10-Mar-07 23:38 
QuestionTape operation Visual Studio 2005 Pin
ASO12315-Jan-07 7:50
ASO12315-Jan-07 7:50 
AnswerRe: Tape operation Visual Studio 2005 Pin
Dima Statz26-Sep-10 3:39
Dima Statz26-Sep-10 3:39 
GeneralConfused Pin
reflex@codeproject12-Sep-06 12:48
reflex@codeproject12-Sep-06 12:48 
GeneralRe: Confused Pin
Dima Statz12-Sep-06 19:19
Dima Statz12-Sep-06 19:19 
Questionsame problem also in .Net 2.2 Pin
mstfnoor10-Sep-06 21:24
mstfnoor10-Sep-06 21:24 
I appreciate your effort.

Here is my modified code. I checked that the device is OK and I called the function Load first. but the problem still exists.

Also, I tried your original class in .Net 2.0 environment and reached the same problem.

when write and flush, the error is: "IO operation will not work. Most likely the file will become too long or the handle was not opened to support synchronous IO operations"

when read, the error is "Handle does not support synchronous operations. The parameters to the FileStream constructor may need to be changed to indicate that the handle was opened asynchronously"

i.e. when write, it need synchronous (where it is already so). when read, it needs asynchronous (which gives an error in constructor if I tried).

Also here is my consuming code
For write:


TapeOperator tape = new TapeOperator();
tape.Load(@"\\.\Tape0");

byte[] BS = new byte[n];
FileStream fs = new FileStream("c:\\Mos.txt",FileMode.Open,FileAccess.Read);
fs.Read(BS,0,(int)fs.Length);
fs.Close();

tape.Write(0, BS);
tape.Close();

For read :

TapeOperator tape = new TapeOperator();
tape.Load(@"\\.\Tape0");

byte[] BS = tape.Read(0);
FileStream fs = new FileStream("c:\\result.txt",FileMode.Open, FileAccess.Write);
fs.Write(BS,0,BS.Length);
fs.Close();
tape.Close();


The class after .Net 1.1 modifications:

using System;<br />
using System.IO;<br />
using System.Runtime.InteropServices;<br />
<br />
//using Microsoft.Win32.SafeHandles;<br />
<br />
using System.Text;<br />
<br />
namespace Tape<br />
{<br />
    #region Typedefenitions<br />
    using BOOL = System.Int32;<br />
    #endregion<br />
<br />
    /// <summary><br />
    /// Low level Tape operator<br />
    /// </summary><br />
    public class TapeOperator<br />
    {<br />
        #region Types<br />
<br />
        [StructLayout(LayoutKind.Sequential)] <br />
        private struct MediaInfo<br />
        {<br />
            public long Capacity;<br />
            public long Remaining;<br />
<br />
            public uint BlockSize;<br />
            public uint PartitionCount;<br />
<br />
            public byte IsWriteProtected;<br />
        }<br />
<br />
        [StructLayout( LayoutKind.Sequential )]<br />
        private class DriveInfo<br />
        {<br />
            public byte ECC;<br />
            public byte Compression;<br />
            public byte DataPadding;<br />
            public byte ReportSetMarks;<br />
<br />
            public uint DefaultBlockSize;<br />
            public uint MaximumBlockSize;<br />
            public uint MinimumBlockSize;<br />
            public uint PartitionCount;<br />
<br />
            public uint FeaturesLow;<br />
            public uint FeaturesHigh;<br />
            public uint EATWarningZone;<br />
        }<br />
        #endregion<br />
<br />
        #region Public constnts<br />
        private const short FILE_ATTRIBUTE_NORMAL = 0x80;<br />
        private const short INVALID_HANDLE_VALUE = -1;<br />
        private const uint GENERIC_READ = 0x80000000;<br />
        private const uint GENERIC_WRITE = 0x40000000;<br />
        private const uint CREATE_NEW = 1;<br />
        private const uint CREATE_ALWAYS = 2;<br />
        private const uint OPEN_EXISTING = 3;<br />
        private const uint FILE_ATTRIBUTE_ARCHIVE = 0x00000020;<br />
        private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;<br />
<br />
        private const uint NO_ERROR = 0;<br />
        private const int TAPE_LOAD = 0;<br />
        private const int TAPE_UNLOAD = 1;<br />
<br />
<br />
        private const int TAPE_RELATIVE_BLOCKS = 5;<br />
<br />
        private const int TAPE_LOGICAL_BLOCK = 2;<br />
        private const int TAPE_LOGICAL_POSITION = 1;<br />
<br />
        private const int FALSE = 0;<br />
        private const int TRUE = 0;<br />
<br />
        private const int MEDIA_PARAMS = 0;<br />
        private const int DRIVE_PARAMS = 1;<br />
        #endregion<br />
<br />
        #region PInvoke<br />
        // Use interop to call the CreateFile function.<br />
        // For more information about CreateFile,<br />
        // see the unmanaged MSDN reference library.<br />
        [DllImport( "kernel32.dll", SetLastError = true )]<br />
        private static extern IntPtr CreateFile(<br />
            string lpFileName,<br />
            uint dwDesiredAccess,<br />
            uint dwShareMode,<br />
            IntPtr lpSecurityAttributes,<br />
            uint dwCreationDisposition,<br />
            uint dwFlagsAndAttributes,<br />
            IntPtr hTemplateFile<br />
            );<br />
<br />
        [DllImport( "kernel32", SetLastError = true )]<br />
        private static extern int PrepareTape(<br />
            IntPtr handle,<br />
            int prepareType,<br />
            BOOL isImmediate<br />
            );<br />
<br />
<br />
        [DllImport( "kernel32", SetLastError = true )]<br />
        private static extern int SetTapePosition(<br />
            IntPtr handle,<br />
            int positionType,<br />
            int partition,<br />
            int offsetLow,<br />
            int offsetHigh,<br />
            BOOL isImmediate<br />
            );<br />
<br />
        [DllImport( "kernel32", SetLastError = true )]<br />
        private static extern int GetTapePosition(<br />
            IntPtr handle,<br />
            int positionType,<br />
            out int partition,<br />
            out int offsetLow,<br />
            out int offsetHigh<br />
            );<br />
<br />
        [DllImport( "kernel32", SetLastError = true )]<br />
        private static extern int GetTapeParameters(<br />
           IntPtr handle,<br />
           int operationType,<br />
           ref int size,<br />
           IntPtr mediaInfo<br />
           );<br />
<br />
        [DllImport( "kernel32", SetLastError = true )]<br />
        private static extern int GetLastError();<br />
        #endregion<br />
<br />
        #region Private variables<br />
        private FileStream m_stream;<br />
<br />
		private IntPtr m_handleValue = IntPtr.Zero;<br />
        private DriveInfo m_driveInfo = null; <br />
        #endregion<br />
<br />
        #region Public methods<br />
        <br />
        /// <summary><br />
        /// Loads tape with given name. <br />
        /// </summary><br />
        public void Load( string tapeName )<br />
        {<br />
            // Try to open the file.<br />
            m_handleValue = CreateFile(<br />
                tapeName,<br />
                GENERIC_READ | GENERIC_WRITE,<br />
                0,<br />
                IntPtr.Zero,<br />
                OPEN_EXISTING,<br />
                FILE_ATTRIBUTE_ARCHIVE | FILE_FLAG_BACKUP_SEMANTICS,<br />
                IntPtr.Zero<br />
                );<br />
<br />
            if ( m_handleValue == IntPtr.Zero )<br />
            {<br />
                throw new TapeOperatorWin32Exception(<br />
                    "CreateFile", Marshal.GetLastWin32Error() );<br />
            }<br />
<br />
            // Load the tape<br />
            int result = PrepareTape(<br />
                m_handleValue,<br />
                TAPE_LOAD,<br />
                TRUE<br />
                );<br />
<br />
            if ( result != NO_ERROR )<br />
            {<br />
                throw new TapeOperatorWin32Exception(<br />
                     "PrepareTape", Marshal.GetLastWin32Error() );<br />
            }<br />
<br />
            m_stream = new FileStream(<br />
                m_handleValue,<br />
                FileAccess.ReadWrite,<br />
				true,<br />
                65536,<br />
				false<br />
                );<br />
        }<br />
<br />
        /// <summary><br />
        /// Writes to the tape given stream starting from given postion<br />
        /// </summary><br />
        /// <param name="startPos"></param><br />
        /// <param name="stream"></param><br />
        public void Write( long startPos, byte[] stream )<br />
        {<br />
            // Get number of blocks that will be nned to perform write<br />
            uint numberOfBlocks = GetBlocksNumber( stream.Length );<br />
<br />
            // Updates tape's current position<br />
            SetTapePosition( startPos );<br />
            <br />
            byte[] arrayToWrite = new byte[ numberOfBlocks * BlockSize ];<br />
            Array.Copy( stream, arrayToWrite, stream.Length );<br />
<br />
            // Write data to the device<br />
            m_stream.Write( stream, 0, stream.Length );<br />
            m_stream.Flush();<br />
        }<br />
<br />
        /// <summary><br />
        /// Read one logical block from tape <br />
        /// starting on the given position<br />
        /// </summary><br />
        /// <returns></returns><br />
        public byte[] Read( long startPosition )<br />
        {<br />
            byte[] buffer = new byte[ BlockSize ];<br />
<br />
            SetTapePosition( startPosition );<br />
            <br />
			m_stream.Read( buffer, 0, buffer.Length );<br />
            m_stream.Flush();<br />
<br />
            return buffer;<br />
        }<br />
<br />
        /// <summary><br />
        /// Read given number of bytes starting <br />
        /// on the given position<br />
        /// </summary><br />
        public byte[] Read( long startPosition, long bytes )<br />
        {<br />
            uint blocksNumber  = GetBlocksNumber( bytes );<br />
            int module = Convert.ToInt32( bytes % BlockSize ); <br />
            <br />
            byte[] buffer = new byte[ bytes ];<br />
<br />
            for ( uint i = 0; i < blocksNumber; i++ )<br />
            {<br />
                byte[] temp = Read( i + startPosition );<br />
<br />
                if ( i + 1 != blocksNumber )<br />
                {<br />
                    Array.Copy( temp, 0, buffer, BlockSize * i, BlockSize );<br />
                }<br />
                else<br />
                {<br />
                    Array.Copy( temp, 0, buffer, BlockSize * i, module );    <br />
                }<br />
<br />
            }// for<br />
<br />
            return buffer;<br />
        }<br />
<br />
        /// <summary><br />
        /// Checks if tape can be read from the<br />
        /// given position<br />
        /// </summary><br />
        public bool CanRead( long startPosition )<br />
        {<br />
            bool status = true;<br />
            long pos = GetTapePosition();<br />
<br />
            try<br />
            {<br />
                Read( startPosition );<br />
            }<br />
            catch<br />
            {<br />
                status = false;<br />
            }<br />
            finally<br />
            {<br />
                SetTapePosition( pos );<br />
            }<br />
<br />
            return status;<br />
        }<br />
<br />
        /// <summary><br />
        /// Checks if given number of bytes can be read<br />
        /// </summary><br />
        public bool CanRead( long startPosition, long bytes )<br />
        {<br />
            bool status = true;<br />
            long pos = GetTapePosition();<br />
<br />
            try<br />
            {<br />
                Read( startPosition, bytes );<br />
            }<br />
            catch<br />
            {<br />
                status = false;<br />
            }<br />
            finally<br />
            {<br />
                SetTapePosition( pos );<br />
            }<br />
<br />
            return status;<br />
        }<br />
        <br />
        /// <summary><br />
        /// Closes handler of the current tape<br />
        /// </summary><br />
        public void Close()<br />
        {<br />
            if (m_handleValue != IntPtr.Zero)<br />
            {<br />
                m_handleValue = IntPtr.Zero;<br />
            }<br />
        }<br />
<br />
        /// <summary><br />
        /// Sets new tape position ( current seek )<br />
        /// </summary><br />
        /// <param name="logicalBlock"></param><br />
        public void SetTapePosition( long logicalBlock )<br />
        {<br />
            int errorCode = 0;<br />
<br />
            // TODO: reapit it<br />
            if ( ( errorCode = SetTapePosition(<br />
               m_handleValue,<br />
               TAPE_LOGICAL_BLOCK,<br />
               0,<br />
               ( int )logicalBlock,<br />
               0,<br />
               TRUE ) ) != NO_ERROR )<br />
            {<br />
                throw new TapeOperatorWin32Exception(<br />
                    "SetTapePosition", Marshal.GetLastWin32Error() );<br />
            }<br />
        }<br />
<br />
        /// <summary><br />
        /// Returns Current tape's postion ( seek )<br />
        /// </summary><br />
        /// <returns></returns><br />
        public long GetTapePosition()<br />
        {<br />
            int partition;<br />
            int offsetLow;<br />
            int offsetHigh;<br />
<br />
            if ( GetTapePosition(<br />
                m_handleValue,<br />
                TAPE_LOGICAL_POSITION,<br />
                out partition,<br />
                out offsetLow,<br />
                out offsetHigh ) != NO_ERROR )<br />
            {<br />
                throw new TapeOperatorWin32Exception(<br />
                    "GetTapePosition", Marshal.GetLastWin32Error() );<br />
            }<br />
<br />
            long offset = ( long )( offsetHigh * Math.Pow( 2, 32 ) + offsetLow );<br />
<br />
            return offset;<br />
        }<br />
        #endregion<br />
<br />
        #region Public properties<br />
        <br />
        /// <summary><br />
        /// Retruns opened file handle<br />
        /// </summary><br />
        public IntPtr Handle<br />
        {<br />
            get<br />
            {<br />
                // If the handle is valid,<br />
                // return it.<br />
                if ( m_handleValue != IntPtr.Zero )<br />
                {<br />
                    return m_handleValue;<br />
                }<br />
                else<br />
                {<br />
                    return IntPtr.Zero;<br />
                }<br />
            }// GET<br />
        }<br />
<br />
        /// <summary><br />
        /// Returns defualt block size for current<br />
        /// device<br />
        /// </summary><br />
        public uint BlockSize<br />
        {<br />
            get<br />
            {<br />
                IntPtr ptr = IntPtr.Zero;<br />
                try<br />
                {<br />
                    if ( m_driveInfo== null )<br />
                    {<br />
                        m_driveInfo = new DriveInfo();<br />
<br />
                        // Allocate unmanaged memory<br />
                        int size = Marshal.SizeOf( m_driveInfo );<br />
                        ptr = Marshal.AllocHGlobal( size );<br />
<br />
                        Marshal.StructureToPtr(<br />
                            m_driveInfo,<br />
                            ptr,<br />
                            false<br />
                        );<br />
<br />
                        <br />
                        int result = 0;<br />
                        if ( ( result = GetTapeParameters(<br />
                            m_handleValue,<br />
                            DRIVE_PARAMS,<br />
                            ref size,<br />
                            ptr ) ) != NO_ERROR )<br />
                        {<br />
                            throw new TapeOperatorWin32Exception(<br />
                                "GetTapeParameters", Marshal.GetLastWin32Error() );        <br />
                        }<br />
<br />
                        // Get managed media Info<br />
                        m_driveInfo = ( DriveInfo )<br />
                            Marshal.PtrToStructure( ptr, typeof( DriveInfo ) );<br />
                    }<br />
<br />
                    <br />
                    return m_driveInfo.DefaultBlockSize;<br />
                }<br />
                finally<br />
                {<br />
                    if ( ptr != IntPtr.Zero )<br />
                    {<br />
                        Marshal.FreeHGlobal( ptr );<br />
                    }<br />
                }<br />
            }<br />
        }<br />
        #endregion<br />
<br />
        #region Private methods<br />
        <br />
        /// <summary><br />
        /// Returns minum number of blocks that can contain<br />
        /// given number of bytes<br />
        /// </summary><br />
        private uint GetBlocksNumber(long bytes)<br />
        {<br />
            uint numberOfBlocks = ( uint )bytes / BlockSize;<br />
            uint bytesInLastBlock = ( uint )bytes % BlockSize;<br />
<br />
            // Calculate number of blocks<br />
            if ( bytesInLastBlock > 0 ) numberOfBlocks++;<br />
<br />
            return numberOfBlocks;<br />
        }<br />
        #endregion<br />
    }<br />
<br />
    /// <summary><br />
    /// Exception that will be thrown by tape<br />
    /// operator when one of WIN32 APIs terminates <br />
    /// with error code <br />
    /// </summary><br />
    public class TapeOperatorWin32Exception : ApplicationException<br />
    {<br />
        public TapeOperatorWin32Exception( string methodName, int win32ErroCode ):<br />
            base( string.Format(<br />
                "WIN32 API method failed : {0} failed with error code {1}",<br />
                methodName,<br />
                win32ErroCode<br />
            ) ){}<br />
    }<br />
<br />
}



-- modified at 6:56 Tuesday 12th September, 2006
AnswerRe: same problem also in .Net 2.2 Pin
Dima Statz12-Sep-06 11:04
Dima Statz12-Sep-06 11:04 
AnswerRe: same problem also in .Net 2.2 Pin
Dima Statz18-Sep-06 22:35
Dima Statz18-Sep-06 22:35 
GeneralThank you. But... Pin
mstfnoor10-Sep-06 4:43
mstfnoor10-Sep-06 4:43 
GeneralRe: Thank you. But... Pin
Dima Statz10-Sep-06 19:44
Dima Statz10-Sep-06 19:44 
GeneralRe: Thank you. But... Pin
lammatron20005-Jul-07 0:32
lammatron20005-Jul-07 0:32 
GeneralRe: Thank you. But... Pin
Dima Statz26-Sep-10 3:21
Dima Statz26-Sep-10 3:21 
QuestionIs there for C# 2003 ? Pin
mstfnoor6-Sep-06 22:01
mstfnoor6-Sep-06 22:01 
AnswerRe: Is there for C# 2003 ? Pin
Dima Statz7-Sep-06 1:06
Dima Statz7-Sep-06 1:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.