ASP.NET 数据绑定包装类的框架

发布于 2024-12-20 20:51:07 字数 1510 浏览 1 评论 0原文

显然 ASP.NET 不允许将数据绑定到动态对象。真是太糟糕了,因为我发现这样的语法非常有用:

public class User
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

...

// No this doesn't exist, I just wish it did!
MyGrid.DataSource = GetAllUsers()
    .AsDynamic()
        .WithProperty("FullName", user => user.FirstName + " " + user.LastName)
    .ToEnumerable(); // returns IEnumerable<dynamic>
MyGrid.DataBind()

...

<asp:BoundField DataField="FirstName" HeaderText="First Name" />
<asp:BoundField DataField="LastName" HeaderText="Last Name" />
<asp:BoundField DataField="FullName" HeaderText="Full Name" />

在本例中,AsDynamic() 将返回一个类,该类将配置 .ToEnumerable( 返回的动态对象) ) 稍后(因为 您无法实现 IEnumerable) 有效地将属性添加到包装的数据对象。对 FirstName 和 LastName 的请求将由真实对象“提供”,而对 FullName 的请求将被路由到要动态评估的委托或表达式。

这是一个简单的示例,因为在大多数情况下,您可以轻松地将 FullName 属性添加到 User 对象,并且可以使用 TemplatedField 轻松实现此目的。

但是,如果添加的属性在没有几行数据绑定代码隐藏的情况下很难在 TemplatedField 中实现,该怎么办?如果您不控制 User 类的源代码怎么办?或者,如果您无法将该属性添加到 User,因为它的计算依赖于一个程序集,而该程序集本身又依赖于 User 的程序集,该怎么办? (循环引用问题)

因此,拥有一个像这样非常易于应用的数据绑定包装器会很棒,您不必每次都生成一个全新的类。

那么我真正追求的是什么?

有没有任何框架或技术可以实现这种事情?上面的确切语法并不重要,只是能够动态向类添加内容并在数据绑定中使用这些代理,而无需一堆手动管道代码。

Apparently ASP.NET doesn't allow data-binding to dynamic objects. Major bummer, because I could see a syntax like this being really useful:

public class User
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

...

// No this doesn't exist, I just wish it did!
MyGrid.DataSource = GetAllUsers()
    .AsDynamic()
        .WithProperty("FullName", user => user.FirstName + " " + user.LastName)
    .ToEnumerable(); // returns IEnumerable<dynamic>
MyGrid.DataBind()

...

<asp:BoundField DataField="FirstName" HeaderText="First Name" />
<asp:BoundField DataField="LastName" HeaderText="Last Name" />
<asp:BoundField DataField="FullName" HeaderText="Full Name" />

In this example, AsDynamic() would return a class that would configure the dynamic objects that would be returned by .ToEnumerable() later (because you can't implement IEnumerable<dynamic>) effectively adding properties to the wrapped data object. The requests for FirstName and LastName would be "served" by the real object, and the request for FullName would be routed to a delegate or expression to be evaluated dynamically.

This is a trivial example, because in most cases you could easily add a FullName property to the User object, and you could easily pull this off with a TemplatedField.

But what if the added property was way too difficult to implement in a TemplatedField without several lines of databinding codebehind? And what if you didn't control the source code for the User class? Or what if you can't add the property to User because its calculation is dependent on an assembly which itself depends on User's assembly? (circular reference problem)

For this reason it would be great to have a very easy-to-apply data binding wrapper such as this, where you don't have to generate a brand new class every single time.

So what am I really after?

Are there any frameworks or techniques out there that allow this kind of thing? The exact syntax above isn't really important, just the ability to dynamically add stuff to classes and use those proxies in data-binding, without a bunch of manual plumbing code.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

反话 2024-12-27 20:51:07

我找到了使用 C# 解决(某些)问题的三种方法,以及使用 Visual Studio 工具扩展其中一些方法的方法。

匿名类型

ASP.NET 可以将数据绑定到匿名类型

DataGrid.DataSource = GetAllUsers().
  .AsQueryable()
  .Select(u => new { User = u, FullName = GetFullName(u) });
DataGrid.DataBind()

匿名类型仍然可以轻松访问原始类型(在本例中通过 User 属性)。这将使数据绑定相对简单(使用 ),并且您已将复杂的逻辑移至在 User 上运行的单独方法对象。

<%# Eval("User.FirstName") %>
<%# Eval("User.LastName") %>
<%# Eval("FullName") %>

数据绑定语法应放置在 ItemTemplate 内,但为了简洁起见,我省略了该代码。当然,最后一个属性也可以使用 显示:

<asp:BoundField DataField="FullName" />

请注意,您不必在匿名中映射原始类型的 each 属性类型,您可以只将一个属性映射到原始对象。 (唯一的?)缺点是您不能再对这些属性使用 ,但必须使用

扩展方法

要补充此方法,您可以使用扩展方法来“附加” ' 即使您无权访问类,也可以使用该类的方法' 源:

public static class UserExtensions
{
  public static string GetFullName(this User user)
  {
      return user.FirstName + " " + user.LastName;
  }
}

对于数据绑定,我们必须使用

<%# Eval("User.FirstName") %>
<%# Eval("User.LastName") %>
<%# (Container.DataItem as User).GetFullName() %>

部分类

另一种选择,自 C# 起可用2.0,就是写一个部分类,但前提是原始类也声明为部分的,并且在您的项目中声明(同一模块的一部分)。如果 User 类是使用工具生成的,例如,如果您在项目中使用某种自动数据库映射器工具,则此方法非常有用。

public partial class User
{
    public string FullName
    {
        get { return this.FirstName + " " + this.LastName; }
    }
}

对于数据绑定,我们现在回到使用 '':

<asp:BoundField DataField="FirstName" />
<asp:BoundField DataField="LastName" />
<asp:BoundField DataField="FullName" />

这些都是 C# 编译器和 .NET 运行时的可能性,因此它们属于技术类别而不是框架类别。当然,也可以使用基本继承,但它可能不适用于您的情况?

T4 文本模板

如果您对数据绑定类的外观有非常具体的需求,但无法使用上述任何方法,您可以随时查看 T4 模板。 (它们适用于 ASP.NET Web 应用程序项目,但不适用于 ASP.NET 网站项目。)

使用这些模板,您可以在设计时生成代码,例如创建一个浅层部分类 UserViewModel,将所有属性透明地映射到内部 User 对象。然后,使用分部类方法,您可以使用 .cs 文件中的另一个分部类声明向此类型添加额外的属性和方法,并且只需针对您的 UserViewModel 进行数据绑定:

DataGrid.DataSource = GetAllUsers().
  .AsQueryable()
  .Select(u => new UserViewModel(u));
DataGrid.DataBind()

数据绑定再次变得简单使用

<asp:BoundField DataField="FirstName" />
<asp:BoundField DataField="LastName" />
<asp:BoundField DataField="FullName" />

使用 T4 模板,您可以为所有域类型自动生成这些自定义视图模型类。在 T4 中使用反射时,有一些注意事项:

I find three ways to solve (some) of your problems using C# and a way to extend some of these approaches using Visual Studio tools.

Anonymous types

ASP.NET can data bind to anonymous types:

DataGrid.DataSource = GetAllUsers().
  .AsQueryable()
  .Select(u => new { User = u, FullName = GetFullName(u) });
DataGrid.DataBind()

The anonymous type can still give easy access to the original type (in this example through the User property). This will make data binding relatively easy (using <asp:TemplateField>), and you have moved the complex logic to a separate method that operates on a User object.

<%# Eval("User.FirstName") %>
<%# Eval("User.LastName") %>
<%# Eval("FullName") %>

The data binding syntax should be placed inside the ItemTemplate of the <asp:TemplateField>, but I have omitted that code for brevity. Of course the last property can also be displayed using the <asp:BoundField>:

<asp:BoundField DataField="FullName" />

Notice that you don't have to map each property of the original type in the anonymous type, you can just map one property to the original object. The (only?) drawback is that you can no longer use <asp:BoundField> for those properties but you must use <asp:TemplateField>.

Extension methods

To complement this approach, you could use extension methods to 'attach' methods to a class even when you don't have access to the class' source:

public static class UserExtensions
{
  public static string GetFullName(this User user)
  {
      return user.FirstName + " " + user.LastName;
  }
}

For data binding we must use <asp:TemplateField>:

<%# Eval("User.FirstName") %>
<%# Eval("User.LastName") %>
<%# (Container.DataItem as User).GetFullName() %>

Partial classes

Another option, available since C# 2.0, is to write a partial class, but only if the original class is also declared partial and is declared in your project (part of the same module). This approach is useful if the User class is generated with a tool, for instance if you use some kind of automatic database mapper tool in your project.

public partial class User
{
    public string FullName
    {
        get { return this.FirstName + " " + this.LastName; }
    }
}

For data binding we are now back to using '':

<asp:BoundField DataField="FirstName" />
<asp:BoundField DataField="LastName" />
<asp:BoundField DataField="FullName" />

These are all possibilities of the C# compiler and the .NET runtime, so they fall in the category of techniques instead of frameworks. Of course, basic inheritance could also be used, but it may not be applicable in your situation?

T4 Text Templates

If you have very specific needs about how the data bound class should look like but can't use any of the approaches above, you can always look into T4 templates in Visual Studio. (They work in ASP.NET Web Application projects but not in ASP.NET Web Site projects.)

With these templates you can generate code at design time, for instance to create a shallow, partial class UserViewModel that maps all properties to an internal User object transparently. Then, using the partial class approach, you can add extra properties and methods to this type using another partial class declaration in a .cs file and simply data bind against your UserViewModel:

DataGrid.DataSource = GetAllUsers().
  .AsQueryable()
  .Select(u => new UserViewModel(u));
DataGrid.DataBind()

Data bind becomes straight-forward again using <asp:BoundField>:

<asp:BoundField DataField="FirstName" />
<asp:BoundField DataField="LastName" />
<asp:BoundField DataField="FullName" />

Using T4 templates you could generate these custom view model classes automatically for all your domain types. When using reflection in T4 there are caveats:

黑色毁心梦 2024-12-27 20:51:07

您可能想查看 Clay 库(请参阅 这个伟大的概述):

public interface IUser {
    public string FirstName { get; set; }
    public string LastName  { get; set; }
    public string FullName  { get; set; }
} 

dynamic New = new ClayFactory();
existingUser = //grab your existing user here
IUser clayUser = New.User(){
    FirstName: existingUser.FirstName,
    LastName: existingUser.LastName,
    FullName: existingUser.FirstName + " " + existingUser.LastName;

当然,给猫剥皮的方法不止一种,尤其是在语法上。另外,我还没有深入研究它(这是你的工作!;)所以我不知道 Clay 对象是否可以覆盖到现有对象上,或者你是否需要从现有对象中填充新的 Clay User像我一样。最重要的是,如果您从接口继承它们,Clay 对象就会存在于 CLR 中,获得智能感知,并且行为就像真正的非动态对象(如果我没看错的话)。

You might want to look into the Clay library (see this great overview):

public interface IUser {
    public string FirstName { get; set; }
    public string LastName  { get; set; }
    public string FullName  { get; set; }
} 

dynamic New = new ClayFactory();
existingUser = //grab your existing user here
IUser clayUser = New.User(){
    FirstName: existingUser.FirstName,
    LastName: existingUser.LastName,
    FullName: existingUser.FirstName + " " + existingUser.LastName;

Of course, there is more than one way to skin that cat, especially syntactically. Also, I haven't dug into it very deeply (that's your job! ;) so I don't know if a Clay object can glom onto an existing object, or if you'll need to populate the new Clay User from the existing one like I did. The great part is, if you inherit them from an interface, Clay objects live in the CLR, get Intellisense, and act just like real non-dynamic objects, if I'm reading the article right.

南城追梦 2024-12-27 20:51:07

实现此目的的一种方法是使用 asp:TemplateField。

您还可以使用 Dynamic Linq 来执行此操作,请参阅 ScottGu 关于 Dynamic Linq 基础知识的博客

然后,您可以使用 Dynamic Linq 创建语句的动态选择部分。下面是一些用于创建 select 语句的代码,该语句选择所有基础对象的属性并基于动态表达式创建额外的属性。

public class ExtraProperty
{
    public string Name { get; set; }
    public string Expression { get; set; }
}


/// <summary>
/// Creates a string on the form "new (property1, property2, ..., expression1 as extraproperty1, ... )
/// </summary>
/// <param name="t"></param>
/// <param name="extraProperties"></param>
/// <returns></returns>
public string CreateSelectClauseWithProperty(Type objecType, ExtraProperty[] extraProperties)
{
    string ret = "new(";
    bool notFirst = false;
    System.Reflection.PropertyInfo[] typeProps = objecType.GetProperties();


    // Equivalent of "Select objectType.*"
    foreach (System.Reflection.PropertyInfo p in typeProps)
    {
        if (notFirst)
            ret += ",";
        else
            notFirst = true;
        ret += p.Name;
    }

    // Equivalent of "expression1 as name1, expression2 as name2, ..." - giving the extra columns
    foreach (ExtraProperty ep in extraProperties)
    {
        if (notFirst)
            ret += ",";
        else
            notFirst = true;
        ret += ep.Expression + " as " + ep.Name;
    }
    return ret + ")";
}

其使用示例如下:

    MyGrid.AutoGenerateColumns = false;
    string selectClause = CreateSelectClauseWithProperty(typeof(User),
            new ExtraProperty[] { 
                    new ExtraProperty() 
                    { Name = "FullName", Expression = "FirstName + \" \" + LastName" }
                }
                );
    IQueryable<User> list = GetAllUsers();
    var query = list.Select( selectClause );
    MyGrid.DataSource =  query;
    MyGrid.DataBind();

您需要将其包含在标头中:

using System.Linq.Dynamic;

One way to achieve this is to use asp:TemplateField.

You can also use Dynamic Linq to do this, see ScottGu's blog about the basics of Dynamic Linq.

You could then use Dynamic Linq to create a dynamic select part of a statement. Here is some code for creating a select statement that select all the base object's properties and creates extra properties based on dynamic expressions.

public class ExtraProperty
{
    public string Name { get; set; }
    public string Expression { get; set; }
}


/// <summary>
/// Creates a string on the form "new (property1, property2, ..., expression1 as extraproperty1, ... )
/// </summary>
/// <param name="t"></param>
/// <param name="extraProperties"></param>
/// <returns></returns>
public string CreateSelectClauseWithProperty(Type objecType, ExtraProperty[] extraProperties)
{
    string ret = "new(";
    bool notFirst = false;
    System.Reflection.PropertyInfo[] typeProps = objecType.GetProperties();


    // Equivalent of "Select objectType.*"
    foreach (System.Reflection.PropertyInfo p in typeProps)
    {
        if (notFirst)
            ret += ",";
        else
            notFirst = true;
        ret += p.Name;
    }

    // Equivalent of "expression1 as name1, expression2 as name2, ..." - giving the extra columns
    foreach (ExtraProperty ep in extraProperties)
    {
        if (notFirst)
            ret += ",";
        else
            notFirst = true;
        ret += ep.Expression + " as " + ep.Name;
    }
    return ret + ")";
}

A sample use of this would be like this:

    MyGrid.AutoGenerateColumns = false;
    string selectClause = CreateSelectClauseWithProperty(typeof(User),
            new ExtraProperty[] { 
                    new ExtraProperty() 
                    { Name = "FullName", Expression = "FirstName + \" \" + LastName" }
                }
                );
    IQueryable<User> list = GetAllUsers();
    var query = list.Select( selectClause );
    MyGrid.DataSource =  query;
    MyGrid.DataBind();

You need to include this in your header:

using System.Linq.Dynamic;
街角迷惘 2024-12-27 20:51:07

阅读 Jesse Smith 关于 Clay 库的回答后,我研究了 Clay,发现它并不真正适合我所追求的目标。然而,Clay 在内部使用 Castle Project 的 DynamicProxy 库,这确实有一些有趣的东西虽然它并不完美,但肯定接近我希望存在的东西。

Castle DynamicProxy 可以通过发出代码来创建对象的代理,然后拦截对其的调用。对业务对象的唯一要求是方法和属性需要标记为虚拟的,以便 Castle 拦截对它们的调用。

然后,您可以将“mixins”添加到代理对象中。我将用问题中的 User 示例进行演示:

public class User
{
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
}

如果我们想将 FullName 添加到该对象的代理中,那么我们需要做一些工作来实现这一点,方法是创建一个声明该属性的接口,并且然后是一个可以提供现有用户值的实现对象:

public interface IUserProxy
{
    string FullName { get; }
}

public class UserProxyImpl : IUserProxy
{
    public User User { get; set; }

    public string FullName
    {
        get { return User.FirstName + " " + User.LastName; }
    }
}

现在对于数据绑定,我真的很想在可枚举上使用它,因此扩展方法可以完成创建代理和添加混合的工作。我们将允许调用代码使用 Func 提供 mixin(基本上只是对象),以便我们可以使用 lambda 表达式定义它们:

public static class ProxyExtensions
{
    public static IEnumerable<T> ProxyAddMixins<T>(this IEnumerable<T> collection, params Func<T, object>[] mixinSelectors)
        where T : class
    {
        ProxyGenerator factory = new ProxyGenerator();
        foreach (T item in collection)
        {
            ProxyGenerationOptions o = new ProxyGenerationOptions();
            foreach (var func in mixinSelectors)
            {
                object mixin = func(item);
                o.AddMixinInstance(mixin);
            }
            yield return factory.CreateClassProxyWithTarget<T>(item, o);
        }
    }
}

然后是我们的客户端代码(我在 Windows 控制台应用程序中进行模拟,因为它更容易测试)可以如下所示。当我们到达提供 mixin 的 lambda 时,我们返回一个新的 UserProxyImpl,并将基本的 User 对象传递给它。 Castle 分析 UserProxyImpl,注意到它实现了 IUserProxy,并导致发出的代理类使用该实现来实现该接口。所有其他属性都会流向原始对象的虚拟实现,而不会被代理拦截。

class Program
{
    static void Main(string[] args)
    {
        List<User> users = new List<User>();
        users.Add(new User { FirstName = "John", LastName = "Doe" });
        users.Add(new User { FirstName = "Jane", LastName = "Doe" });

        var userProxies = users
            .ProxyAddMixins(u => new UserProxyImpl { User = u })
            .ToList();

        Console.WriteLine("First\tLast\tFull");
        foreach (var userProxy in userProxies)
        {
            Console.WriteLine("{0}\t{1}\t{2}",
                DataBinder.Eval(userProxy, "FirstName"),
                DataBinder.Eval(userProxy, "LastName"),
                DataBinder.Eval(userProxy, "FullName"));
        }
        Console.ReadLine();
    }
}

我真的想要一些东西,你可以只通过定义一些 lambda 来创建代理,而不需要定义额外的接口或实现类,但这似乎是唯一的方法。当然,您必须考虑相对于其他方法是否真的值得发出这些自定义类型来完成这项工作。

这是完整代码的要点,因此您无需将其组装即可提供尝试一下。

After reading Jesse Smith's answer regarding the Clay library, I looked into Clay and decided that it wasn't really suited to what I'm after. However, internally Clay uses the Castle Project's DynamicProxy library, and that does have some interesting stuff in it that, while not perfect, certainly approaches what I wish existed.

A Castle DynamicProxy can create a proxy of an object by emitting code, and then intercepting calls to it. About the only requirement on your business objects is that methods and properties need to be marked as virtual in order for Castle to intercept calls to them.

You can then add "mixins" to your proxy object. I'll demonstrate with the example of User from the question:

public class User
{
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
}

If we want to add FullName to a proxy of this object, then we need to do a little bit of plumbing to enable that, by creating an interface that declares the property, and then an implementation object that can supply the value from the existing user:

public interface IUserProxy
{
    string FullName { get; }
}

public class UserProxyImpl : IUserProxy
{
    public User User { get; set; }

    public string FullName
    {
        get { return User.FirstName + " " + User.LastName; }
    }
}

Now for data-binding, I'd really like to use this on an enumerable, so an extension method can do the work of creating the proxy and adding mixins. We'll allow the calling code to provide the mixins (which are basically just objects) using a Func<T, object> so that we can define them using lambda expressions:

public static class ProxyExtensions
{
    public static IEnumerable<T> ProxyAddMixins<T>(this IEnumerable<T> collection, params Func<T, object>[] mixinSelectors)
        where T : class
    {
        ProxyGenerator factory = new ProxyGenerator();
        foreach (T item in collection)
        {
            ProxyGenerationOptions o = new ProxyGenerationOptions();
            foreach (var func in mixinSelectors)
            {
                object mixin = func(item);
                o.AddMixinInstance(mixin);
            }
            yield return factory.CreateClassProxyWithTarget<T>(item, o);
        }
    }
}

Then our client code (which I'm mocking up in a Windows Console app because it's just easier to test) can look like this. When we get to the lambda for providing the mixins, we return a new UserProxyImpl passing in the base User object to it. Castle analyzes the UserProxyImpl, notices that it implements IUserProxy, and causes the emitted proxy class to implement that interface with that implementation. All other properties flow through to the original object's virtual implementations without being intercepted by the proxy.

class Program
{
    static void Main(string[] args)
    {
        List<User> users = new List<User>();
        users.Add(new User { FirstName = "John", LastName = "Doe" });
        users.Add(new User { FirstName = "Jane", LastName = "Doe" });

        var userProxies = users
            .ProxyAddMixins(u => new UserProxyImpl { User = u })
            .ToList();

        Console.WriteLine("First\tLast\tFull");
        foreach (var userProxy in userProxies)
        {
            Console.WriteLine("{0}\t{1}\t{2}",
                DataBinder.Eval(userProxy, "FirstName"),
                DataBinder.Eval(userProxy, "LastName"),
                DataBinder.Eval(userProxy, "FullName"));
        }
        Console.ReadLine();
    }
}

I really wanted something where you could just create the proxy just by defining some lambdas without defining an additional interface or implementation class, but this seems to be the only way to do that. Of course you have to take into consideration if it's really worth emitting these custom types to do this work vs. other methods.

Here is a gist of the he full code, so you don't have to assemble it to give it a try.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文