3 of 10 Added an step-by-step explanation

Java, 318 312 bytes

This program won't work on systems that interpret CRLF as two newlines instead of one.

interface a{static void main(String[]A){if(A[0].equals("1")&A[1].equals("1")){System.out.print('o');return;}int b=Byte.valueOf(A[0]),B=Byte.valueOf(A[1]);String c="";for(int C=3+b*2;C>0;C--)c+="_";for(;B>0;B--){c+="\n|";for(int C=b;C>0;C--)c+=" o";c+=" |\r";}for(int C=3+b*2;C>0;C--)c+="-";System.out.print(c);}}

It doesn't crash when one of the/both dimensions is/are 0.

Ungolfed In a human-readable form:

interface a {
    static void main(String[] A) {
        if (A[0].equals("1") & A[1].equals("1")) {
            System.out.print('o');
            return;
        }
        int b = Byte.valueOf(A[0]),
            B = Byte.valueOf(A[1]);
        String c = "";
        for (int C = 3 + b*2; C > 0; C--)
            c += "_";
        for(; B > 0; B--) {
            c += "\n|";
            for(int C = b; C > 0; C--)
                c+=" o";
            c += " |\r";
        }
        for (int C = 3 + b*2; C > 0; C--)
            c += "-";
        System.out.print(c);
    }
}

Yes, it's still difficult to understand what's going on even when the program is ungolfed. So here goes a step-by-step explanation:

static void main(String[] A)

The first two command line arguments -which we'll use to get dimensions- can be used in the program as A[0] and A[1] (respectively).

if (A[0].equals("1") & A[1].equals("1")) {
    System.out.print('o');
    return;
}

If the piece to be printed is 1x1, then both A[0] and A[1] should be one-character strings which contain 1 as a character. As a result, we print o when A[0] equals to "1" and A[1] equals to "1".

The return statement is an unbelievably stupid hack which terminates the program.

int b = Byte.valueOf(A[0]),
    B = Byte.valueOf(A[1]);

Here, we parse the dimensions1, assuming A[0] represents the columns and A[1] represents the rows.

String c = "";

This is the Lego piece. We'll append the rows to it and then print it at the end.

for (int C = 3 + b*2; C > 0; C--)
    c += "_";

Here, we append (integerValueOfA[0] * 2) + 3 underscores to c. This is the topmost row above all holes.

for (; B > 0; B--) {
    c += "\n|";
    for(int C = b; C > 0; C--)
        c+=" o";
    c += " |\r";
}

This is the loop where we construct the piece one row at a time. What's going on inside is impossible to explain without examples. Let's say that the piece is 4x4:

Before entering the loop, c looks like this:
___________

After the first iteration (\n denotes a line feed and \r denotes a carriage return):
___________\n
| o o o o |\r

After the second iteration:
___________\n
| o o o o |\r\n
| o o o o |\r

After the third iteration:
___________\n
| o o o o |\r\n
| o o o o |\r\n
| o o o o |\r

.

for (int C = 3 + b*2; C > 0; C--)
    c += "-";

Here, we append (integerValueOfA[0] * 2) + 3 hyphens to the piece. This is the row at the very bottom, below all holes.

The 4x4 piece I used for explaining the for loop where the piece is actually constructed now looks like this:

___________\n
| o o o o |\r\n
| o o o o |\r\n
| o o o o |\r\n
| o o o o |\r
-----------

.

System.out.print(c);

And finally, we print the piece!