2

我有很多看起来像这样的丑陋代码:

if (!string.IsNullOrEmpty(ddlFileName.SelectedItem.Text))
    results = results.Where(x => x.FileName.Contains(ddlFileName.SelectedValue));
if (chkFileName.Checked)
    results = results.Where(x => x.FileName == null);

if (!string.IsNullOrEmpty(ddlIPAddress.SelectedItem.Text))
    results = results.Where(x => x.IpAddress.Contains(ddlIPAddress.SelectedValue));
if (chkIPAddress.Checked)
    results = results.Where(x => x.IpAddress == null);

...etc.

results是一个IQueryable<MyObject>
这个想法是,对于这些无数下拉列表和复选框中的每一个,如果下拉列表中选择了某些内容,则用户希望匹配该项目。如果选中该复选框,则用户特别想要该字段为空或空字符串的那些记录。(UI 不允许同时选择两者。)这一切都添加到 LINQ 表达式中,在我们添加了所有条件之后,该表达式将在最后执行。

似乎应该有某种方法可以抽出一两个,以便我可以将重复的部分放入一个方法中,然后传递更改的内容Expression<Func<MyObject, bool>>我在其他地方做过这个,但是这组代码让我受阻。(另外,我想避免“动态 LINQ”,因为如果可能的话,我想保持类型安全。)有什么想法吗?

4

10 回答 10

5

我会将其转换为单个 Linq 语句:

var results =
    //get your inital results
    from x in GetInitialResults()
    //either we don't need to check, or the check passes
    where string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) ||
       x.FileName.Contains(ddlFileName.SelectedValue)
    where !chkFileName.Checked ||
       string.IsNullOrEmpty(x.FileName)
    where string.IsNullOrEmpty(ddlIPAddress.SelectedItem.Text) ||
       x.FileName.Contains(ddlIPAddress.SelectedValue)
    where !chkIPAddress.Checked ||
       string.IsNullOrEmpty(x. IpAddress)
    select x;

它并不短,但我发现这个逻辑更清晰。

于 2008-09-10T19:13:48.627 回答
5

在这种情况下:

//list of predicate functions to check
var conditions = new List<Predicate<MyClass>> 
{
    x => string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) ||
         x.FileName.Contains(ddlFileName.SelectedValue),
    x => !chkFileName.Checked ||
         string.IsNullOrEmpty(x.FileName),
    x => string.IsNullOrEmpty(ddlIPAddress.SelectedItem.Text) ||
         x.IpAddress.Contains(ddlIPAddress.SelectedValue),
    x => !chkIPAddress.Checked ||
         string.IsNullOrEmpty(x.IpAddress)
}

//now get results
var results =
    from x in GetInitialResults()
    //all the condition functions need checking against x
    where conditions.All( cond => cond(x) )
    select x;

我刚刚明确声明了谓词列表,但可以生成这些,例如:

ListBoxControl lbc;
CheckBoxControl cbc;
foreach( Control c in this.Controls)
    if( (lbc = c as ListBoxControl ) != null )
         conditions.Add( ... );
    else if ( (cbc = c as CheckBoxControl ) != null )
         conditions.Add( ... );

您需要某种方法来检查您需要检查的 MyClass 的属性,为此您必须使用反射。

于 2008-09-11T07:35:45.240 回答
1

如果它影响可读性,请不要使用 LINQ。将各个测试分解为布尔方法,可用作您的 where 表达式。

IQueryable<MyObject> results = ...;

results = results
    .Where(TestFileNameText)
    .Where(TestFileNameChecked)
    .Where(TestIPAddressText)
    .Where(TestIPAddressChecked);

因此,单个测试是类上的简单方法。它们甚至可以单独进行单元测试。

bool TestFileNameText(MyObject x)
{
    return string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) ||
           x.FileName.Contains(ddlFileName.SelectedValue);
}

bool TestIPAddressChecked(MyObject x)
{
    return !chkIPAddress.Checked ||
        x.IpAddress == null;
}
于 2008-09-27T01:18:27.320 回答
1

你见过LINQKit吗?AsExpandable 听起来像您所追求的(尽管您可能想阅读TomasP.NET 的LINQ 查询中的调用函数以获得更深入的了解)。

于 2008-09-11T08:20:44.400 回答
0

您可能会考虑的一件事是通过消除复选框并在下拉列表中使用“ <empty>”或“ ”项来简化 UI。<null>这将减少占用窗口空间的控件数量,消除对复杂的“仅在未选中 Y 时启用 X”逻辑的需要,并且将启用一个很好的每个查询字段一个控件。


继续您的结果查询逻辑,我将首先创建一个简单的对象来表示您的域对象上的过滤器:

interface IDomainObjectFilter {
  bool ShouldInclude( DomainObject o, string target );
}

您可以将过滤器的适当实例与您的每个 UI 控件相关联,然后在用户启动查询时检索它:

sealed class FileNameFilter : IDomainObjectFilter {
  public bool ShouldInclude( DomainObject o, string target ) {
    return string.IsNullOrEmpty( target )
        || o.FileName.Contains( target );
  }
}

...
ddlFileName.Tag = new FileNameFilter( );

然后,您可以通过简单地枚举控件并执行关联的过滤器来概括您的结果过滤(感谢hurst的聚合想法):

var finalResults = ddlControls.Aggregate( initialResults, ( c, r ) => {
  var filter = c.Tag as IDomainObjectFilter;
  var target = c.SelectedValue;
  return r.Where( o => filter.ShouldInclude( o, target ) );
} );


由于您的查询非常常规,您可以通过使用带有成员选择器的单个过滤器类来进一步简化实现:

sealed class DomainObjectFilter {
  private readonly Func<DomainObject,string> memberSelector_;
  public DomainObjectFilter( Func<DomainObject,string> memberSelector ) {
    this.memberSelector_ = memberSelector;
  }

  public bool ShouldInclude( DomainObject o, string target ) {
    string member = this.memberSelector_( o );
    return string.IsNullOrEmpty( target )
        || member.Contains( target );
  }
}

...
ddlFileName.Tag = new DomainObjectFilter( o => o.FileName );
于 2008-09-27T00:37:55.420 回答
0
results = results.Where(x => 
    (string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) || x.FileName.Contains(ddlFileName.SelectedValue))
    && (!chkFileName.Checked || string.IsNullOrEmpty(x.FileName))
    && ...);
于 2008-09-10T18:41:32.450 回答
0

到目前为止,这些答案都不是我想要的。为了举例说明我的目标(我也不认为这是一个完整的答案),我采用了上面的代码并创建了几个扩展方法:

static public IQueryable<Activity> AddCondition(
    this IQueryable<Activity> results,
    DropDownList ddl, 
    Expression<Func<Activity, bool>> containsCondition)
{
    if (!string.IsNullOrEmpty(ddl.SelectedItem.Text))
        results = results.Where(containsCondition);
    return results;
}
static public IQueryable<Activity> AddCondition(
    this IQueryable<Activity> results,
    CheckBox chk, 
    Expression<Func<Activity, bool>> emptyCondition)
{
    if (chk.Checked)
        results = results.Where(emptyCondition);
    return results;
}

这使我可以将上面的代码重构为:

results = results.AddCondition(ddlFileName, x => x.FileName.Contains(ddlFileName.SelectedValue));
results = results.AddCondition(chkFileName, x => x.FileName == null || x.FileName.Equals(string.Empty));

results = results.AddCondition(ddlIPAddress, x => x.IpAddress.Contains(ddlIPAddress.SelectedValue));
results = results.AddCondition(chkIPAddress, x => x.IpAddress == null || x.IpAddress.Equals(string.Empty));

这不是那么难看,但它仍然比我想要的要长。每组中的 lambda 表达式对显然非常相似,但我想不出一种进一步压缩它们的方法……至少在不求助于动态 LINQ 的情况下是这样,这让我牺牲了类型安全。

还有其他想法吗?

于 2008-09-10T22:37:06.580 回答
0

@Kyralessa,

您可以为接受 Control 类型的参数加上 lambda 表达式并返回组合表达式的谓词创建扩展方法 AddCondition。然后,您可以使用流畅的接口组合条件并重用您的谓词。要查看如何实现它的示例,请参阅我对这个问题的回答:

如何编写现有的 Linq 表达式

于 2008-09-10T23:28:44.427 回答
0

我会警惕以下形式的解决方案:

// from Keith
from x in GetInitialResults()
    //either we don't need to check, or the check passes
    where string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) ||
       x.FileName.Contains(ddlFileName.SelectedValue)

我的推理是变量捕获。如果您立即执行一次,您可能不会注意到差异。但是,在 linq 中,评估不是立即的,而是在每次迭代发生时发生。代表可以捕获变量并在您想要的范围之外使用它们。

感觉就像您查询的 UI 离用户界面太近了。查询是向下的一层,而 linq 不是 UI 向下通信的方式。

您最好执行以下操作。将搜索逻辑与表示分离——它更灵活和可重用——OO 的基础。

// my search parameters encapsulate all valid ways of searching.
public class MySearchParameter
{
    public string FileName { get; private set; }
    public bool FindNullFileNames { get; private set; }
    public void ConditionallySearchFileName(bool getNullFileNames, string fileName)
    {
        FindNullFileNames = getNullFileNames;
        FileName = null;

        // enforce either/or and disallow empty string
        if(!getNullFileNames && !string.IsNullOrEmpty(fileName) )
        {
            FileName = fileName;
        }
    }
    // ...
}

// search method in a business logic layer.
public IQueryable<MyClass> Search(MySearchParameter searchParameter)
{
    IQueryable<MyClass> result = ...; // something to get the initial list.

    // search on Filename.
    if (searchParameter.FindNullFileNames)
    {
        result = result.Where(o => o.FileName == null);
    }
    else if( searchParameter.FileName != null )
    {   // intermixing a different style, just to show an alternative.
        result = from o in result
                 where o.FileName.Contains(searchParameter.FileName)
                 select o;
    }
    // search on other stuff...

    return result;
}

// code in the UI ... 
MySearchParameter searchParameter = new MySearchParameter();
searchParameter.ConditionallySearchFileName(chkFileNames.Checked, drpFileNames.SelectedItem.Text);
searchParameter.ConditionallySearchIPAddress(chkIPAddress.Checked, drpIPAddress.SelectedItem.Text);

IQueryable<MyClass> result = Search(searchParameter);

// inform control to display results.
searchResults.Display( result );

是的,它需要更多的打字,但是你阅读的代码比你写的多 10 倍。您的 UI 更清晰,搜索参数类自行处理并确保互斥选项不会发生冲突,并且搜索代码从任何 UI 中抽象出来,甚至根本不关心您是否使用 Linq。

于 2008-09-25T00:27:47.217 回答
0

由于您希望使用无数过滤器反复减少原始结果查询,因此可以使用Aggregate()(对应于函数式语言中的 reduce() )。

根据我从您的帖子中收集的信息,过滤器具有可预测的形式,由 MyObject 的每个成员的两个值组成。如果要比较的每个成员都是一个字符串,可能为 null,那么我建议使用扩展方法,它允许将空引用关联到其预期类型的​​扩展方法。

public static class MyObjectExtensions
{
    public static bool IsMatchFor(this string property, string ddlText, bool chkValue)
    {
        if(ddlText!=null && ddlText!="")
        {
            return property!=null && property.Contains(ddlText);
        }
        else if(chkValue==true)
        {
            return property==null || property=="";
        }
        // no filtering selected
        return true;
    }
}

我们现在需要在一个集合中安排属性过滤器,以允许迭代多个。它们表示为与 IQueryable 兼容的表达式。

var filters = new List<Expression<Func<MyObject,bool>>>
{
    x=>x.Filename.IsMatchFor(ddlFileName.SelectedItem.Text,chkFileName.Checked),
    x=>x.IPAddress.IsMatchFor(ddlIPAddress.SelectedItem.Text,chkIPAddress.Checked),
    x=>x.Other.IsMatchFor(ddlOther.SelectedItem.Text,chkOther.Checked),
    // ... innumerable associations
};

现在我们将无数过滤器聚合到初始结果查询中:

var filteredResults = filters.Aggregate(results, (r,f) => r.Where(f));

我在带有模拟测试值的控制台应用程序中运行它,它按预期工作。我认为这至少说明了这个原则。

于 2008-09-25T08:40:52.070 回答