To answer your question as to why some property accesses raise an illegal cross-thread error and other do not, you can refer to the source code for System.Windows.Forms.Control. It takes bit of digging through it, but it does appear as though some access is suggested as as being thread-safe such as getting the Text property but setting it is not.

In reality, the conventional wisdom to treat all control access as not thread safe is the best practice.

/// <devdoc>
/// The current text of the Window; if the window has not yet been created, stores it in the control.
/// If the window has been created, stores the text in the underlying win32 control.
/// This property should be used whenever you want to get at the win32 control's text. For all other cases,
/// use the Text property - but note that this is overridable, and any of your code that uses it will use
/// the overridden version in controls that subclass your own.
/// </devdoc>
internal virtual string WindowText {
get {
if (!IsHandleCreated) {
if (text == null) {
return "";
}
else {
return text;
}
}
using (new MultithreadSafeCallScope()) {
// it's okay to call GetWindowText cross-thread.
//
int textLen = SafeNativeMethods.GetWindowTextLength(new HandleRef(window, Handle));
// Check to see if the system supports DBCS character
// if so, double the length of the buffer.
if (SystemInformation.DbcsEnabled) {
textLen = (textLen * 2) + 1;
}
StringBuilder sb = new StringBuilder(textLen + 1);
UnsafeNativeMethods.GetWindowText(new HandleRef(window, Handle), sb, sb.Capacity);
return sb.ToString();
}
}
set {
if (value == null) value = "";
if (!WindowText.Equals(value)) {
if (IsHandleCreated) {
UnsafeNativeMethods.SetWindowText(new HandleRef(window, Handle), value);
}
else {
if (value.Length == 0) {
text = null;
}
else {
text = value;
}
}
}
}
}

Note the use of MultithreadSafeCallScope in the get code. Also note the use of the Handle property that will throw a cross-thread error; I believe the Handle property acts as the gate-keeper that checks for cross-thread access.