Saturday, January 13, 2007

A guy in the newsgroups who is new to .NET, coming from VB6, is having a hard time drawing on a Form.  The problem with VB6 was it had the Shape controls which provided a crutch for a developer to never actually understand how drawing worked, and now that crutch has been removed.

Drawing is simple - you need a Graphics object to paint on.  The easiest way to get that for your Form is to just override OnPaint - you'll get it as an input parameter.  The other nice effect is that you don't have to do anything other than refresh the Form to get your code to run.

Now normally I don't like to just give out the answer - no one learns much that way - but he seems to genuinely have spent several hours trying to get this, so I figured I'd throw him a bone so he doesn't get frustrated and give up altogether.  So the goal is to draw a "crosshair" on the Form that the user can move around with the D-Pad on the device.  Again, this is a very, very basic example - it took me roughly 15 minutes to do (and that's becasue my VB is very rusty).

Create a device WinForms app.  Select the Form and make sure KeyPreview is True.  Then add this code:

Public Class Form1
   Private crosshairs As New Rectangle(0, 0, 20, 20)
   Private bluePen As New Pen(Color.Blue)
   Private Const StepBy As Int32 = 3

Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
   Dim b As New SolidBrush(Color.Blue)

   ' let the system do it's normal drawing
   MyBase.OnPaint(e)

   'now draw our crosshairs
   DrawCrosshairs(e.Graphics)
End Sub

Private Sub DrawCrosshairs(ByRef g As Graphics)
   g.DrawEllipse(bluePen, crosshairs)
   g.DrawLine( bluePen, _
               crosshairs.Left + crosshairs.Width / 2, _
               
crosshairs.Top - 10, _
               crosshairs.Left + crosshairs.Width / 2, _
               crosshairs.Bottom + 10)
   g.DrawLine( bluePen, _
               crosshairs.Left - 10, _
               crosshairs.Top + crosshairs.Height / 2, _
               crosshairs.Right + 10, _
               crosshairs.Top + crosshairs.Height / 2)
End Sub

Private Sub Form1_KeyDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyDown
   If (e.KeyCode = System.Windows.Forms.Keys.Up) Then
      If crosshairs.Top >= StepBy Then
         crosshairs.Offset(0, -StepBy)
      End If
   End If
   If (e.KeyCode = System.Windows.Forms.Keys.Down) Then
      If crosshairs.Bottom <= Me.Height - StepBy Then
         crosshairs.Offset(0, StepBy)
     End If
   End If
   If (e.KeyCode = System.Windows.Forms.Keys.Left) Then
      If crosshairs.Left >= StepBy Then
         crosshairs.Offset(-StepBy, 0)
      End If
   End If
   If (e.KeyCode = System.Windows.Forms.Keys.Right) Then
      If crosshairs.Right <= Me.Width - StepBy Then
         crosshairs.Offset(StepBy, 0)
      End If
   End If
   'this forces OnPaint to be called
   Me.Refresh()
End Sub
End Class

That's all there is to it.

1/13/2007 6:18:19 PM (Eastern Standard Time, UTC-05:00)  #    Comments [1]  | 
 Friday, January 12, 2007

This question comes up a lot, and the well-known and documented workaround is to set your Form's Text property to an empty string to hide it from the Running Programs applet so I won't go into that.  And before reading further, if you're after hiding a Form, then go Google for that info - everything I present here are ways that are proven to not work.  It's simply info for those who are tempted to try another route.

We know that the Running Programs applet simply enumerates all top-level windows with text.  We also know that a lot of native apps don't have the problem of child windows showing up, so I decided to try to mimic that behavior with some P/Invoke shenanigans.  The test app simply had 2 forms - Form1 would create a Form2 instance on a button click and then call ShowDialog on the new Form2 instance.  After that (if it got that far), I'd check the applet to see if my code worked.

Method 1: Reparent the Form with SetParent

[DllImport("coredll.dll", SetLastError = true)]
internal static extern IntPtr SetParent(IntPtr hwndChild, IntPtr hwndNewParent);

  • Tried calling this after creating the Form2 and before calling ShowDialog.  ShowDialog subsequently throws an ArgumentException
  • Tried calling it in the ctor of Form2. ShowDialog subsequently throws an ArgumentException
  • Tried passing Form1's Handle to Form 2, then calling SetParent in a Form2.Activate handler. ShowDialog subsequently throws an ArgumentException
  • Tried calling SetParent from a button click in Form2, so after the dialog is loaded and shown.  No exception now, but when SetParent runs, Form1 gains scrollbars, Form2's controls end up in Form1 along with Form1's controls (so a mash up of both) and all controls are non-responsive.

Method 2: Change the Window Style bits

internal const int GWL_STYLE = -16;
internal const int GWL_EXSTYLE = -20;
internal const uint WS_CHILD = 0x40000000;
internal const uint WS_EX_APPWINDOW = 0x00040000;

[DllImport("coredll.dll", SetLastError = true)]
internal static extern uint SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);

[DllImport("coredll.dll", SetLastError = true)]
internal static extern uint GetWindowLong(IntPtr hWnd, int nIndex);

  • Tried setting style to WS_CHILD after creation and before ShowDialog.  Still appears in applet.
  • Tried setting style to WS_CHILD in Form2 ctor.  Still appears in applet.
  • Tried setting style to WS_CHILD in Form2 Activate handler.  Still appears in applet.
  • Tried setting style to WS_CHILD in Form2 button click handler.  Still appears in applet.
  • Tried unsetting extended style to WS_EX_APPWINDOW after creation and before ShowDialog.  Still appears in applet.
  • Tried unsetting extended style to WS_EX_APPWINDOW in Form2 button handler.  Still appears in applet.

So what's the take away lesson?  CF Forms like to be shown in that damned applet, and the simplest mechanism is still to just set the text to an empty string when you want to hide it.

1/12/2007 2:10:24 PM (Eastern Standard Time, UTC-05:00)  #    Comments [1]  | 

When we use a language, often we tend to overlook some of the more obvious constructs or be frustrated by what we think should work.  For example, assume we have this simple problem - our function receives a value and based on that value we run through a switch, but we have code that will be run for multiple cases.  Explaining what I mean in words is tough - so let's look at what I'm trying to say in code.  Assume we have these enums:

[Flags]
enum Foo
{
  NoFoo = 0,
  FooA = 1,
  FooB = 2,
  FooC = 4
}

enum Bar
{
  A,
  B,
  C
}

We want a function that will take in a Bar, and based on that create a Foo.  If Bar is A, the the resulting Foo is a combination of FooA, FooB and FooC.  If Bar is B, then it's a combination of FooB and FooC.  If Bar is C, then the result is just FooC.  In C, we'd just do this:

Foo FooBar(Bar bar)
{
  Foo foo = Foo.NoFoo;

  switch (bar)
  {
    case Bar.A:
      foo |= Foo.FooA;
    case Bar.B:
      foo |= Foo.FooB;
    case Bar.C:
      foo |= Foo.FooC;
    break;
  }
  return foo;
}

Letting each case fall into the next intentionally. Yes it's a contrived example, but you get the idea.  There are cases when we need to do processing like this (like a project I'm doing right now).

Well C# doesn't like this type of construct - I'm not certain why it's illegal (other than missing breaks are common bugs) - but the compiler will say 'Error: Control cannot fall through from one case label ('case 0:') to another'.  So you might code a 'fix' like this:

Foo FooBar(Bar bar)
{
   Foo foo = Foo.NoFoo;

   switch (bar)
   {
      case Bar.A:
         foo |= Foo.FooA;
      break;
      case Bar.B:
         foo |= Foo.FooA;
         foo |= Foo.FooB;
      break;
      case Bar.C:
         foo |= Foo.FooA;
         foo |= Foo.FooB;
         foo |= Foo.FooC;
      break;
   }
   return foo;
}

Not too bad, but if you have to do more processing than a single line it gets ugly and maintainability goes downhill fast.

The thing to keep in mind in C# is that those case statements are actually labels, so you can use them as such, meaning they are valid goto targets, so this code is perfectly valid:

Foo FooBar3(Bar bar)
{
   Foo foo = Foo.NoFoo;

   switch (bar)
   {
      case Bar.A:
         foo |= Foo.FooA;
         goto case Bar.B;
      case Bar.B:
         foo |= Foo.FooB;
         goto case Bar.C;
      case Bar.C:
         foo |= Foo.FooC;
      break;
   }
   return foo;
}

1/12/2007 12:34:40 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 
 Wednesday, January 10, 2007

Bugs are frustrating, but when you write code for a living you expect them and live with them as a fact of life.  They are a lot harder to swallow when they are introduced by other libraries, especially when those libraries work counter to what you would expect.  It's even more frustrating when it's in a library like the Compact Framework itself.

Recently we got a bug report for our ConnectionManager in some code that I know we tested - basically the Description returned by the DestinationInfo class was returning odd data and concatenating to it did bad things.  Experience told me that it sounded like the data coming back from the native API was not getting properly truncated at a NULL.  Not an unusual mistake, so I went to find it.

Description = Marshal2.PtrToStringUni(baseAddr, 16, 256);
int nullPos = Description.IndexOf('\0'
);
if (nullPos > -1) Description = Description.Substring(0, nullPos);

The bad news was the code looked right - we were looking for NULL and trimming at it.  The only way a problem could arise is if the buffer was non-zero at the start.  So off to look at that code.  Here's where the allocation is made:

hDestInfo = Marshal.AllocHGlobal(DestinationInfo.NativeSize);

Again, it looked right.  However, talking with Alex Feinman he said that the Marshal Alloc functions do no zero memory.  In the earlier versions of this code we used our internal MarshalEx, which P/Invoked LocalAlloc with the LPTR parameter, which zeros everything at allocation.  Why the hell the CF doesn't do that one can only guess, but it makes no sense in my book.  Who would want to allocate memory and not zero it?  Worse still is that MSDN doesn't say that the memory is not zeroed.

So then, changing from our MarshalEx function to the CF's Marshal function introduced an error.  So how do we fix it?  There's no CF equivalent to a memset (again, this is a WTF in my book, but there are a few language limitations like this that irritate me - try taking an IntPtr and turning the data at that location into a managed struct without having to copy it).

So anyway, I looked at coredll.def from Platform Builder 5.0 to see if coredll.dll helps, and sure enough I see this:

malloc @1041
calloc @1346
_memccpy @1042
memcmp @1043
memcpy @1044
_memicmp @1045
memmove @1046
memset @1047

P/Invoke to the rescue.  I added the following to the SDF (so it will be in the next release).  Note that wile I was there I handles the annoyances of not having a way to copy from an IntPtr to an IntPtr (memcpy) or a way to validate IntPtrs (IsBad[Read/Write]Ptr) while I was at it.

public static void SetMemory(IntPtr destination, byte value, int length)
public static void SetMemory(IntPtr destination, byte value, int length, bool boundsCheck)
public static void Copy(IntPtr source, IntPtr destination, int length)
public static void Copy(IntPtr source, IntPtr destination, int length, bool boundsCheck)
public static bool IsSafeToWrite(IntPtr destination, int length)
public static bool IsSafeToRead(IntPtr source, int length)

Then a simple fix back in the ConnectionManager:

hDestInfo = Marshal.AllocHGlobal(DestinationInfo.NativeSize);
Marshal2.SetMemory(hDestInfo, 0, DestinationInfo.NativeSize, false);

 

1/10/2007 9:51:46 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 
 Tuesday, January 09, 2007

So this morning we got an email from PayPal:

The PayPal User Agreement states that PayPal, at its sole discretion, reserves the right to limit an account for any violation of the User Agreement, including the Acceptable Use Policy. Under the Acceptable Use Policy, PayPal may not be used to send or receive payments or donations for obscene or certain sexually oriented goods or services. The complete Acceptable Use Policy addressing Mature Audiences can be found at the following URL:

http://www.paypal.com/cgi-bin/webscr?cmd=p/gen/ua/use/index_frame-outside&ed=mature

We are hereby notifying you that, after a recent review of your account activity, it has been determined that you are in violation of PayPal's Acceptable Use Policy regarding your website:

http://www.opennetcf.org/forums/topic.asp?TOPIC_ID=8609. Therefore, your account has been permanently limited.

If you have a remaining balance, you may withdraw the funds to your bank account. Information on how to withdraw funds from your PayPal Account can be found at our Help Center.

You will need to remove all references to PayPal from your website(s) and/or auction(s). This includes not only removing PayPal as a payment option, but also the PayPal logo and/or shopping cart. We thank you in advance for your cooperation. If you have any questions, please contact the PayPal Acceptable Use Policy Department at aup@paypal.com.

Sincerely,

PayPal Acceptable Use Policy Department PayPal, an eBay Company

Right off you can tell that they didn't like something in our public Forums - a place where anyone who can enter a user name can post anything they'd like.  Offhand I don't even know what was at that topic ID as Neil deleted it once he saw this notification.  We also use the PayPal account very, very rarely - I'd guess there were probably 5 transactions in the last 12 months, so how the hell they figured some violation by "reviewing account activity" is beyond my comprehension.

So Neil replid to them that we had no activity of the sort and that the material was removed.  Shortly after that we got this:

Dear Chris Tacke,

Based on the information provided to you in our last email your account has been permanently closed. Under the Acceptable Use Policy, PayPal may not be used to send or receive payments or donations for obscene or certain sexually oriented goods or services. The complete Acceptable Use Policy addressing Mature Audiences can be found at the following URL:

http://www.paypal.com/cgi-bin/webscr?cmd=p/gen/ua/use/index_frame-outside&ed=mature

Unfortunately, we will be unable to overturn the limitation on your account. I do apologize for any inconvenience this issue may be causing you at this time.

Sincerely,
PayPal Acceptable Use Policy Department
PayPal an eBay Company

Nice.  One "infraction" of material, which we didn't post (again spammers are the lowest form of life) and which we removed, and the close the account with really no appeals process.  Not only do they charge fees way above what any other merchant provider charges and not only do they tend to monopolize eBay commerce, they evidently also employ a large number of morons. I like how they term "permanently closed" as a "limitation on your account."

How does a company like this stay in business?

1/9/2007 11:13:53 AM (Eastern Standard Time, UTC-05:00)  #    Comments [1]  | 
 Monday, January 08, 2007

There's a thread in the newsgroups where someone is trying to show a Notification before his app does some long-running process. Here's an example of how it's done.

  1. Create a WM 5.0 Windows app, then add a reference to 'Microsoft.WindowsCE.Forms'.
  2. Add a single Button to the Form and name it 'workButton'
  3. Add this to the top of the code page:

    using System.Threading;
    using Microsoft.WindowsCE.Forms;

  4. Replace the entire non-designer Form class code with this:

    public partial class Form1 : Form
    {
      public Form1()
      {
        InitializeComponent();
        workButton.Click += new System.EventHandler(workButton_Click);
      }

      delegate void EventDelegate();

      Notification m_workNotify = new Notification();
      Control m_invoker = new Control();
      EventDelegate m_workCompleteDelegate;

      private void workButton_Click(object sender, EventArgs e)
      {
        // disable the button so it can't be clicked again until work is done
        workButton.Enabled = false;
        m_workCompleteDelegate = new EventDelegate(OnWorkComplete);

        Thread workThread = new Thread(new ThreadStart(WorkProc));
        m_workNotify.Text = "I'm doing important stuff";
        m_workNotify.Caption = "Please wait...";
        m_workNotify.InitialDuration = 5;

        m_workNotify.Visible = true;

        workThread.Start();
      }

      void OnWorkComplete()
      {
        // re-enable the button
        workButton.Enabled = true;
      }

      void WorkProc()
      {
        // simulate working
        Thread.Sleep(20000);
        m_invoker.Invoke(m_workCompleteDelegate);
      }
    }

1/8/2007 2:51:27 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  |