0

I have a class that I am creating within an AppDomain using CreateInstanceAndUnwrap. The class contains a System.Threading.Timer.

The problem that I am having is that when the class is instantiated, the timer's callback method can't seem to see proper values of the class instance.

I have sample code below that illustrates the problem:

Library Class

using System;
using System.Threading;

namespace Library
{
    [Serializable]
    public class Class1
    {
        public Class1()
        {
            Started = false;

            _Timer = new Timer(TimerMethod);
        }

        public bool Started { get; set; }

        private readonly Timer _Timer;
        private string _Message;
        private string _TimerMessage;

        public bool Start()
        {
            Started = true;

            _Message = string.Format("Class1 says Started = {0}", Started);
            _TimerMessage = "Timer message not set yet";

            _Timer.Change(1000, 1000);

            return Started;
        }

        public string GetMessage()
        {
            // _TimerMessage is never set by TimerMethod when this class is created within an AppDomain
            return string.Format("{0}, {1}", _Message, _TimerMessage);
        }

        public void TimerMethod(object state)
        {
            // Started is always false here when this class is created within an AppDomain
            _TimerMessage = string.Format("Timer says Started = {0} at {1}", Started, DateTime.Now);
        }
    }
}

Consumer Class

using System;
using System.Windows.Forms;
using Library;

namespace GUI
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            var appDomainSetup = new AppDomainSetup
            {
                ApplicationName = "GUI",
                ApplicationBase = AppDomain.CurrentDomain.BaseDirectory
            };

            _AppDomain = AppDomain.CreateDomain(appDomainSetup.ApplicationName,
                                                AppDomain.CurrentDomain.Evidence,
                                                appDomainSetup);

            _Class1 = _AppDomain.CreateInstanceAndUnwrap("Library", "Library.Class1") as Class1;
        }

        private readonly AppDomain _AppDomain;
        private readonly Class1 _Class1;

        private void button1_Click(object sender, EventArgs e)
        {
            _Class1.Start();
            MessageBox.Show(_Class1.GetMessage());
        }

        private void button2_Click(object sender, EventArgs e)
        {
            MessageBox.Show(_Class1.GetMessage());
        }
    }
}

When the code above is run, GetMessage() always returns:

Class1 says Started = True, Timer message not set yet

However, if I change to the constructor of the form above to create a local instance of Class1,

        public Form1()
        {
            InitializeComponent();

            _Class1 = new Class1();
        }

GetMessage() returns the expected message:

Class1 says Started = True, Timer says Started = True at 11/15/2011 12:34:06 PM

I have searched Google, MSDN and SO, but haven't found any information that specifically addresses the combination of AppDomain, Serialization and the System.Threading.Timer. Nor could I find any information on why the TimerCallback could not reference local members of the class that instantiated the Timer.

Welton v3.62
  • 2,210
  • 7
  • 29
  • 46
  • When you place the debugger on "_TimerMessage = string.Format("Timer says Started = {0} at {1}", Started, DateTime.Now);" does the code execution ever get there? –  Nov 15 '11 at 18:02
  • Have Class1 derive from MarshalByRefObject to fix the problem. – Hans Passant Nov 15 '11 at 18:06
  • @slfan, In my actual class I have other classes (such as XElement) which are not serializable. For those classes, I receive a SerializationException at run-time that says "Type 'System.Xml.Linq.XElement' ... is not marked as serializable." I would have expected the same for the Timer class. – Welton v3.62 Nov 15 '11 at 18:10
  • @kurtnelle, yes, I can set a breakpoint in the timer callback routine, and when I check Started, it is false. – Welton v3.62 Nov 15 '11 at 18:11

2 Answers2

3

Most likley it is caused by difference between "marshal by value" (your class) and "marshal by reference" (most likely what you want). If class is not derived from MarshalByRefObject than it behaves like values type during remoting, meaning you get copy of the object on each side of communication. If type derives from MarshalByRefObject than you get proxy on one of the side that did not instanitated the object and that side will be able to call methods on the instance in the other AppDomain.

Links:

MarshalByRefObject - http://msdn.microsoft.com/en-us/library/system.marshalbyrefobject.aspx

Lifetime management during cross-app-domain calls article in MSDN magazine - download December 2003 issue of MSDN magazine (you'd likely need to unblock content in file's properties) or use Web archive link Managing the Lifetime of Remote .NET Objects with Leasing and Sponsorship

Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
  • I made the suggested change to my sample code and it worked. However, when I made the same change to my production code I received a SerializationException for my consumer class. In the sample, the consumer class is a form, but in my production code the consumer class is another class that contains a number of non-serializable members. I decided to try descending my consumer class from MarshalByRefObject also, and that seems to have done the trick. Thanks. – Welton v3.62 Nov 15 '11 at 18:41
  • Also, thanks for the LifeTime management reference! That will be very helpful. – Welton v3.62 Nov 15 '11 at 18:48
  • 1
    The magazine link no longer works - according to https://stackoverflow.com/a/25315226/155892 it was in the *December 2003* issue though which can be downloaded in CHM format from the same page. – Mark Sowul Sep 09 '18 at 20:26
0

A comment in your TimerMethod says:

// Started is always false here when this class is created within an AppDomain

But your output says that Started is true.

Which is it?

Actually, I'm kind of surprised that it works when you create a local instance. The Class1 constructor creates the timer and gives it a callback, but it doesn't set the interval or due time, meaning that the timer won't fire.

When you call Start, the timer is initialized, but it's given a 1 second due time. Start returns, and you call GetMessage to get the message. But if you call GetMessage before the timer has a chance to execute its callback, you're going to get the behavior that you describe.

If you put a 1-second delay between calling Start and calling GetMessage, I think you'll see that the problem is one of ... timing: you're trying to get the message before the timer has had a chance to set it. Try the following to verify:

private void button1_Click(object sender, EventArgs e)
{
    _Class1.Start();
    Thread.Sleep(1000);
    MessageBox.Show(_Class1.GetMessage());
}

Or, I suppose you could just try clicking the button again after a couple seconds' delay.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • The message returned from button1_Click was not the issue. That message was expected because the timer hadn't triggered yet. It was the message from button2_Click after a couple seconds had elapsed that showed the problem. And setting a breakpoint in TimerMethod _always_ showed Start to be false when Class1 was created within the AppDomain. – Welton v3.62 Nov 15 '11 at 18:46