¿Hay alguna manera de almacenar un delegado anónimo en un estado de vista?

En una WebControl, i have a property Filters definido así:

public Dictionary<string, Func<T, bool>> Filters
{
    get
    {
       Dictionary<string, Func<T, bool>> filters =
               (Dictionary<string, Func<T, bool>>)ViewState["filters"];
       if (filters == null)
       {
          filters = new Dictionary<string, Func<T, bool>>();
          ViewState["filters"] = filters;
       }
       return filters;
    }
 }

This webcontrol is a DataSource, i created this property because i want to have the possiblity to filter data easily, eg:

//in page load    
DataSource.Filters.Add("userid", u => u.UserID == 8);

It works great, however, if I change code to this :

//in page load    
int userId = int.Parse(DdlUsers.SelectedValue);
DataSource.Filters.Add("userid", u => u.UserID == userId);

It doesn't works anymore, I get this error :

Type System.Web.UI.Page in Assembly '...' is not marked as serializable.

What happened :

  1. The serializer inspect the dictionary. It sees it contains a anonymous delegate (lambda here)
  2. Since the delegate is defined in a class, it tries to serialize the whole class, in this case System.Web.UI.Page
  3. This class is not marked as Serializable
  4. It throws an exception because of 3.

Is there any convenient solution to solve this ? I cannot mark all web pages where i use the datasource as [serializable] for obvious reasons.


EDIT 1 : something I don't understand. If I store the Dictionary en el capítulo respecto a la Session object (which use a BinaryFormatter vs LosFormatter por ViewState), it works ! I have no idea how it is possible. Maybe BinaryFormatter can serialize any class, even these who are not [serializable] ?


EDIT 2 : smallest code to reproduce the problem :

void test()
{
    Test test = new Test();
    string param1 = "parametertopass";
    test.MyEvent += () => Console.WriteLine(param1);

    using (MemoryStream ms = new MemoryStream())
    {
       BinaryFormatter bf = new BinaryFormatter();
       bf.Serialize(ms, test); //bang
    }
}

[Serializable]
public class Test
{
   public event Action MyEvent;
}

preguntado el 28 de agosto de 12 a las 12:08

"it works ! I have no idea how ..." : Session data remains server-side, in-memory. It will start to break when you move to 2+ servers. -

2 Respuestas

Good question. I can confirm your diagnose (with one tiny correction: The infrastructure tries to serialize the closure class which probably contains a reference to your page).

You can define your own closure class and have that serialized:

[Serializable] class Closure { int userId; bool Filter(User u) { ... } };

That is not convenient, though.

I suggest you use a different pattern: Don't serialize "code". Serialize the data used by the filter:

class FilterSettings { int userId; int someOtherFiler; string sortOrder; ... }

Although I cannot point to the exact reason why I would prefer that I intuitively know that is a better approach.

Respondido 28 ago 12, 12:08

Thanks for you answer. I am not sure how second proposition will work. Who will instantiate and hold the instance of FilterSettings class ? If its the page, isn't there any risk that serializer will reach the page as well ? Can you provide more code about how to use this solution ? - tigrou

Storing a class whose fields you precisely control will never reference the page. There is no way the serializer can reach the page starting from Closure or FilterSettings. - usr

I still cannot make it work even with you suggestions. I have added a sample of code at the bottom of question that will reproduce the problem. Can you have a look and give me hint ? - tigrou

I guess the compiler-created closure class is not [Serializable]. That's why you need to roll your own. If you want to serialize a function, make that function an instance member of the closure class and create the delegate like this: new Action(myClosure.InstanceMethod). You can't use a lambda. - usr

I found a solution, here is how i did it :

I modified Dictionary definition like this:

Dictionary<string, KeyValuePair<Func<T, object[], bool>, object[]>>

(KeyValuePair is serializable, just like Dictionary)

y creó un nuevo Add() función:

public void Add(string key, Func<T, object[], bool> filter, params object[] args)
{
    this.Add(key, new KeyValuePair<Func<T, object[], bool>, object[]>
          (filter, args));
}

Now filters can be set this way :

int userId = int.Parse(DdlUsers.SelectedValue);
DataSource.Filters.Add("userid", (u, args) => u.UserID == (int)args[0], userId);

It works, because now captured variables are no more a part the delegate, but given as parameters of the delegate.

Respondido el 21 de diciembre de 12 a las 11:12

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.