2008-09-10 224 views
2

我已經得到了很多醜陋的代碼看起來像這樣:你會如何重構這個LINQ代碼?

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. 

resultsIQueryable<MyObject>
想法是,對於這些無數的下拉菜單和複選框中的每一個,如果下拉列表中選擇了某些內容,則用戶希望匹配該項目。如果選中該複選框,則用戶特別需要那些字段爲空或空字符串的記錄。 (UI不允許同時選擇兩者。)這一切都增加了LINQ表達式,在我們添加完所有條件後,最後執行該表達式。

似乎喜歡,就必須有某種方式拉出一個Expression<Func<MyObject, bool>>或兩個,這樣我可以把重複部分的方法,只是在傳遞什麼樣的變化。我在其他地方做過,但是這組代碼讓我感到困惑。 (另外,我想避免使用「動態LINQ」,因爲如果可能,我想保持類型安全。)任何想法?

回答

0
results = results.Where(x => 
    (string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) || x.FileName.Contains(ddlFileName.SelectedValue)) 
    && (!chkFileName.Checked || string.IsNullOrEmpty(x.FileName)) 
    && ...); 
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; 

這並不短,但我覺得這個邏輯清晰。

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,這使我犧牲了類型安全性。

還有其他想法嗎?

0

@Kyralessa,

可以創建爲謂詞擴展方法AddCondition接受型控制加lambda表達式並返回聯合表達的參數。然後,您可以使用流暢的界面組合條件並重用謂詞。要查看示例的它如何可以實現看到關於這個問題我的回答:

How do I compose existing Linq Expressions

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的屬性,你需要檢查,並且你會hav e使用反射。

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中,評估不是直接的,而是每次迭代都會發生。代表可以捕獲變量並在您想要的範圍之外使用它們。

感覺就像你在查詢的界面太靠近了。查詢是一個層,而linq不是用於UI通信的方式。

你可能最好做以下事情。從演示中分離搜索邏輯 - 它更靈活,可重用 - 面向對象的基礎。

// 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倍。你的用戶界面更清晰,搜索參數類自己照顧自己,並確保相互排斥的選項不會相互衝突,搜索代碼從任何用戶界面抽象出來,甚至根本不關心你是否使用Linq。

0

由於您想重複減少含有無數過濾器的原始結果查詢,因此可以使用Aggregate()(對應於函數式語言中的reduce())。

過濾器是可預測的形式,根據我從您的帖子收集的信息,爲每個MyObject成員組成兩個值。如果要比較的每個成員都是一個字符串(可能爲空),那麼我建議使用擴展方法,該方法允許空引用與其預期類型的​​擴展方法相關聯。

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)); 

我與模擬試驗值的控制檯應用程序運行此,它的工作如預期。我認爲這至少證明了原則。

0

您可能會考慮的一件事是通過取消複選框並在下拉列表中使用「<empty>」或「<null>」項目來簡化您的UI。這將減少佔用窗口空間的控件數量,消除複雜的「僅在未檢查到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); 
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; 
} 
+0

請記住,這是LINQ to SQL(我沒有在問題中說過,但它是其中一個標籤)。我想過濾發生在數據庫端,而不是客戶端。 – 2008-09-30 01:42:39