У меня есть следующий код, который возвращает результаты из таблицы базы данных, состоящей из поля Id и поля Name, и передает их в список SelectListItems (в моем представлении это заполняет раскрывающийся список).
var locationTypes = await APIHelper.GetAsync<List<LocationType>>(url);
var items = new List<SelectListItem>();
items.AddRange(locationTypes.Select(locationType =>
{
var item = new SelectListItem();
item.Value = locationType.LocationTypeId.ToString();
item.Text = locationType.Name;
return item;
}));
Я часто повторяю это в своем приложении, заменяя LocationType на разные другие вещи. Item.Value всегда получает свойство Id возвращаемых данных (поле Id всегда имеет формат {TableName} + «Id»), а item.Text всегда получает свойство «.Name».
Как я могу сделать это универсальным? Я пытаюсь добиться чего-то вроде этого, хотя это синтаксически неверно и может быть неправильным подходом:
var myGenericObjects = await APIHelper.GetAsync<List<T>>(url)
var items = new List<SelectListItem>();
items.AddRange(myGenericObjects .Select(myGenericObjects =>
{
var item = new SelectListItem();
item.Value = myGenericObject.Field[0].ToString();
item.Text = myGenericObject.Name;
return item;
}));
Спасибо, @MarkG - но как я могу сделать lt => lt [0] (т.е. первое поле) вместо того, чтобы указывать LocationTypeId
Вы можете создать собственное расширение для объекта общего списка, а затем с помощью отражения получить значения, которые вы хотите сопоставить с полями SelectListItem.Text и Name. Примечание. Я использую nameof, чтобы избежать путаницы или магических строковых представлений свойств, с которыми я пытаюсь сопоставить.
Я определил значение по умолчанию «Name» для параметра namePropertyName. В соответствии с вашим описанием это звучало так, как будто по соглашению большинство ваших DTO имеют свойство «Имя». Если это не так, просто удалите заданное значение по умолчанию.
Есть дополнительные проверки, которые могут быть выполнены в этом расширении, чтобы предотвратить NullReference и ArgumentExceptions, но для простоты примера они были опущены. Пример. Обеспечение наличия значения в параметрах idPropertyName и namePropertyName и обеспечение наличия этих имен свойств в предоставленном универсальном объекте до преобразования.
public static class ListExtensions
{
public static List<SelectListItem> ToSelectList<T>(this List<T> list, string idPropertyName, string namePropertyName = "Name")
where T : class, new()
{
List<SelectListItem> selectListItems = new List<SelectListItem>();
list.ForEach(item =>
{
selectListItems.Add(new SelectListItem
{
Text = item.GetType().GetProperty(namePropertyName).GetValue(item).ToString(),
Value = item.GetType().GetProperty(idPropertyName).GetValue(item).ToString()
});
});
return selectListItems;
}
}
Пример использования:
var testList = new List<TestDto>
{
new TestDto { Name = "Test0", TestId = 0 },
new TestDto { Name = "Test1", TestId = 1 },
new TestDto { Name = "Test2", TestId = 2 },
new TestDto { Name = "Test3", TestId = 3 },
new TestDto { Name = "Test4", TestId = 4 },
};
var selectList = testList.ToSelectList(nameof(TestDto.TestId), nameof(TestDto.Name));
Вот класс TestDto для справки:
public class TestDto
{
public int TestId { get; set; }
public string Name { get; set; }
}
Это было невероятно полезно, спасибо, любезный сэр! Обратите внимание, что в .Add( ...
я изменил текст, чтобы получить имя, и значение, чтобы получить идентификатор, а не наоборот :)
Я ... поймал это после того, как опубликовал ... не набралось достаточно очков репутации, чтобы исправить это :-D Я рад, что смог помочь!
Нет проблем, впечатляющее начало StackOverflow! Я заметил, что код работает без where T : class, new()
- в чем его предназначение?
Это ограничения типа. «Class» ограничивает «T» типами классов C#, а «new ()» говорит, что для предоставленного класса должен быть определен пустой конструктор. Поскольку большинство Dtos являются простыми классами POCO, можно было безопасно добавить эти ограничения. Вы же не хотите, чтобы кто-то пытался преобразовать List <string> или List <Enum>, если вы знаете, что типы, которые вы пытаетесь преобразовать, являются классами и имеют пустые конструкторы.
Другими словами ... удаление ограничений типа позволит вам передать любой объект List <T>, и именно поэтому их удаление работает для вас, но открывает дверь для кого-то еще, чтобы случайно неправильно использовать метод расширения для несоответствующих объектов.
Это помогает с производительностью или просто для того, чтобы попытаться предоставить какую-то встроенную документацию к методу? Конечно, если кто-то попробует такое безумие, они скоро увидят все ошибки, говоря ... нет, это не работает так :) - извините, я пытаюсь понять реальный мир зверя C#
@egmfrs Если кто-то попробует такое безумие, он увидит ошибки только во время выполнения. Это дает поддержку времени компиляции, и компилятор остановит их.
@CodingYoshi прав, и поэтому вы пытаетесь ограничить использование с помощью ограничений типа и / или добавления дополнительных проверок в метод. Отражение может быть немного волшебством, которое люди не часто хотят иметь в своем коде, однако, если его понять и правильно использовать, это очень мощная функция .Net Framework.
@egmfrs Типовые ограничения существуют только для того, чтобы ограничить доступные типы теми, которые вы хотите разрешить для метода. Прирост производительности за счет удаления ограничений в этом случае ничтожен, а преимущество ограничения метода от неправильного использования намного превышает миллисекунду в производительности, которую вы можете потерять ИМХО.
Некоторая подготовительная работа
Если вы можете изменить имена столбцов таблицы, используйте соглашение. Например, всегда называйте столбец «Значение» «X», а столбец «Текст» - «Y» (дайте им более удачные имена). Затем сделайте так, чтобы все классы для этих таблиц реализовали интерфейс, подобный этому:
public interface ICustomLookup
{
string X { get; set; }
string Y { get; set; }
}
public class SomeClass : ICustomLookup
{
public string X { get; set; }
public string Y { get; set; }
}
Затем такой метод расширения:
public static class EnumerableExtension
{
public static SelectList ToSelectList(this IEnumerable<ICustomLookup> items)
{
return new SelectList(items.Select(thisItem => new SelectListItem
{
Text = thisItem.X,
Value = thisItem.Y
}));
}
}
Применение
var items = new List<SomeClass>
{
new SomeClass { X = "XOne", Y = "YOne" },
new SomeClass { X = "XTwo", Y = "YTwo" }
};
SelectList selectList = items.ToSelectList();
Если бы я мог изменить имена своих столбцов, я бы просто переименовал все идентификаторы в «Id», и тогда мой предложенный код был бы в порядке, если бы я просто использовал myGenericObject.Id вместо myGenericObject.Field[0]
, с которым я боролся. В любом случае спасибо за ваше время и хотя бы урок.
@egmfrs Конечно, не беспокойтесь. Когда вы сказали, что это есть повсюду в вашем проекте, я подумал, что вы можете использовать это соглашение, потому что оно поможет читаемости и понятности кода и проекта. Я также хотел избежать отражения (я склоняюсь к отражению в крайнем случае). Я использовал эту технику много раз, и у нее есть несколько других преимуществ.
Спасибо - Можно ли добавить интерфейс с int id, а затем унаследовать его в моих классах и установить id => MytableId? Или это безумный разговор. И в чем причина избегать размышлений?
@egmfrs Имя столбца таблицы должно совпадать с именем интерфейса. Тогда ваш класс для таблицы автоматически будет удовлетворять интерфейсу. Если они разные, то в реализации интерфейса можно вернуть значение столбца сопоставления. Отражение происходит медленно (медленнее, чем в этом подходе), и ваш проект не будет иметь согласованного дизайна. При таком подходе вы, другие разработчики, пользователи вашего API (если таковые имеются) с легкостью привыкнете к вашему дизайну.
Попробуйте
var data = locationTypes.ToDictionary(lt => lt.LocationTypeId, lt => lt.Name)
, а затем используйтеnew SelectList(data, "Key", "Value")
.