У меня много уродливого кода, который выглядит так:
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>.
Идея состоит в том, что для каждого из этих бесчисленных раскрывающихся списков и флажков, если в раскрывающемся списке что-то выбрано, пользователь хочет сопоставить этот элемент. Если флажок установлен, пользователю нужны именно те записи, в которых это поле имеет значение NULL или пустую строку. (Пользовательский интерфейс не позволяет выбирать оба одновременно.) Все это добавляет к выражению LINQ, которое выполняется в конце, после того как мы добавили все условия.
Это кажется, как будто должен быть какой-то способ вытащить Expression<Func<MyObject, bool>> или два, чтобы я мог поместить повторяющиеся части в метод и просто передать, какие изменения. Я делал это в других местах, но этот набор кода поставил меня в тупик. (Кроме того, я бы хотел избежать "Dynamic LINQ", потому что я хочу, чтобы вещи были безопасными по типу, если это возможно.) Есть идеи?





results = results.Where(x =>
(string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) || x.FileName.Contains(ddlFileName.SelectedValue))
&& (!chkFileName.Checked || string.IsNullOrEmpty(x.FileName))
&& ...);
Я бы преобразовал его в один оператор 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;
Не короче, но мне эта логика понятнее.
Ни один из этих ответов пока не совсем то, что я ищу. Чтобы дать пример того, к чему я стремлюсь (я тоже не считаю это полным ответом), я взял приведенный выше код и создал несколько методов расширения:
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));
Это не такой уродливый довольно, но он все же длиннее, чем я бы предпочел. Пары лямбда-выражений в каждом наборе, очевидно, очень похожи, но я не могу придумать способ их дальнейшего уплотнения ... по крайней мере, не прибегая к динамическому LINQ, который заставляет меня жертвовать безопасностью типов.
Есть другие идеи?
@Kyralessa,
Вы можете создать метод расширения AddCondition для предикатов, который принимает параметр типа Control плюс лямбда-выражение и возвращает комбинированное выражение. Затем вы можете комбинировать условия с помощью свободного интерфейса и повторно использовать свои предикаты. Чтобы увидеть пример того, как это можно реализовать, см. Мой ответ на этот вопрос:
В таком случае:
//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, которое вам нужно проверить, и для этого вам нужно будет использовать отражение.
Вы видели LINQKit? AsExpandable звучит как то, что вам нужно (хотя вы можете прочитать сообщение Вызов функций в запросах LINQ на TomasP.NET для большей глубины).
Я бы с осторожностью относился к решениям вида:
// 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 - это не способ взаимодействия пользовательского интерфейса.
Возможно, вам будет лучше сделать следующее. Отделите логику поиска от презентации - она более гибкая и многоразовая - основы объектно-ориентированного программирования.
// 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 вообще.
Поскольку вы хотите многократно сокращать исходный запрос результатов с помощью бесчисленных фильтров, вы можете использовать Агрегат () (что соответствует 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));
Я запустил это в консольном приложении с смоделированными тестовыми значениями, и он работал, как ожидалось. Думаю, это как минимум демонстрирует принцип.
Одна вещь, которую вы могли бы рассмотреть, - это упростить свой пользовательский интерфейс, убрав флажки и вместо этого используя элемент «<empty>» или «<null>» в раскрывающемся списке. Это уменьшит количество элементов управления, занимающих место в вашем окне, устранит необходимость в сложной логике «включить X, только если Y не отмечен» и позволит создать красивое поле с одним элементом управления для каждого запроса.
Переходя к логике запроса результатов, я бы начал с создания простого объекта, представляющего фильтр для вашего объекта домена:
interface IDomainObjectFilter {
bool ShouldInclude( DomainObject o, string target );
}
Вы можете связать соответствующий экземпляр фильтра с каждым из элементов управления пользовательского интерфейса, а затем получить его, когда пользователь инициирует запрос:
sealed class FileNameFilter : IDomainObjectFilter {
public bool ShouldInclude( DomainObject o, string target ) {
return string.IsNullOrEmpty( target )
|| o.FileName.Contains( target );
}
}
...
ddlFileName.Tag = new FileNameFilter( );
Затем вы можете обобщить фильтрацию результатов, просто перечислив элементы управления и выполнив связанный фильтр (спасибо спешить за идею агрегирования):
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 );
Не используйте 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;
}
Ах! -1 на таком подходе то.
Имейте в виду, что это LINQ to SQL (что я не сказал в вопросе, но это один из тегов). Я хочу, чтобы фильтрация происходила на стороне базы данных, а не на стороне клиента.