I have a Player class and a Stat class. The Player class has a List property where PlayerStat has a List and XP properties. I think my design is flawed because I am having trouble doing things that I think should be easy. I want to list out all Players and their XP for all Stats. Below are more details of the classes and the GetCompletePlayerStats method which is what I really don't like. I basically need to list out the XP for all stats for a Player, if the player doesn't have a stat then it should have an XP of zero. Any design help/suggestions would be appreciated.
public class Stat : EntityBase{
public virtual string Name { get; set; }
public virtual UnitOfMeasure Unit { get; set; }
public virtual int UnitXP { get; set; }
}
public class Player : EntityBase{
public virtual string Name { get; set; }
public virtual IList<PlayerStat> PlayerStats { get; set; }
public virtual List<PlayerStat> GetCompletePlayerStats(IQueryable<Stat> stats)
{
var allStats = new List<PlayerStat>();
var playerStatIds = PlayerStats.Select(ps => ps.PlayerStatistic.Id).ToList();
if (playerStatIds.Count == 0)
{
allStats.AddRange(stats.Select(stat => new PlayerStat() {PlayerStatistic = stat, XP = 0}));
}
else
{
var zeroStats = stats.Where(s => !playerStatIds.Contains(s.Id)).ToList();
allStats.AddRange(zeroStats.Select(zeroStat => new PlayerStat() {PlayerStatistic = zeroStat, XP = 0}));
allStats.AddRange(PlayerStats);
}
return allStats;
}
}
public class PlayerStat : EntityBase{
public virtual Stat PlayerStatistic { get; set; }
public virtual double XP { get; set; }
}
I have to admit, I dont really see a major drawback in your class design so far. Of course I dont have any insight in the greater picture and how your game is designed, since this is only a little section of it.
However, you said it is the GetCompletePlayerStats that you dont really like. I had to read it several times to understand what you are trying to do here. If I saw that right, you just want to return a PlayerStat object corresponding to each given Stat object. I guess Stat has an Id field (you are using it in your method) to compare two of them for semantic equality.
Given, that I made the right assumptions so far (unfortunately, you didnt provide much info), the method can be simplified to something like:
public virtual IEnumerable<PlayerStat> GetCompletePlayerStats(IEnumerable<Stat> stats)
{
foreach(Stat stat in stats)
yield return PlayerStats.FirstOrDefault(s => stat.Id == s.PlayerStatistic.Id) ??
new PlayerStat() {PlayerStatistic = stat, XP = 0};
yield break;
}
This method here doesnt require a IQueryable but rather a IEnumerable to iterate over via a foreach loop, and yield out the corresponding PlayerStats object if there is one, or create a new one with XP set to 0 otherwise (The null-coalescing operator ?? is very useful in those cases).
Hope that helps!
With the existing design, this method can be simplified thus:
public virtual IList<PlayerStat> GetCompletePlayerStats(IEnumerable<Stat> stats)
{
// build a dictionary of existing stats by ID to facilitate
// the join with requested stats (effectively doing a hash join)
var playerStatsById = PlayerStats.ToDictionary(ps => ps.PlayerStatistic.Id);
// for each requested stat, return either the corresponding player stat
// or a zero stat if one isn't found, maintaining the original order of stats
return stats
.Select(s => playerStatsById.ContainsKey(s.Id) ?
playerStatsById[s.Id] :
new PlayerStat { PlayerStatistic = s, XP = 0 })
.ToList();
}
Note that since this is effectively an outer-join operation (you're joining stats with PlayerStats but you also need the non-matches to yield as zero) you can also do it with LINQ's GroupJoin() operation, although I suspect that would be much less readable.
What does bother me about your design is the ambiguity of names, for example you have both the PlayerStat class and the PlayerStatistic property and they mean subtly different things; consider naming things differently and things might look better; in fact that's probably the best design advice I can give you, for any situation :)
Related
I have a class ScoreStrategy that describes how to calculate points for a quiz:
public class ScoreStrategy
{
public int Id { get; set; }
public int QuizId { get; set; }
[Required]
public Quiz Quiz { get; set; }
public decimal Correct { get; set; }
public decimal Incorrect { get; set; }
public decimal Unattempted { get; set; }
}
Three properties Correct, Incorrect and Unattempted describe how many points to be assigned for a response. These points can also be negative. The score strategy applies to all questions in the quiz, thus there can only be one ScoreStrategy per quiz.
I have two subclasses:
public class DifficultyScoreStrategy : ScoreStrategy
{
public QuestionDifficulty Difficulty { get; set; }
}
public class QuestionScoreStrategy : ScoreStrategy
{
[Required]
public Question Question { get; set; }
}
My questions have three difficulty levels(Easy, Medium, Hard; QuestionDifficulty is an enum). The DifficultyScoreStrategy specifies if points for questions of a specific difficulty need to be assigned differently. This overrides the base ScoreStrategy that applies to the entire quiz. There can be one instance per difficulty level.
Thirdly, I have a QuestionScoreStrategy class that specifies if points for a specific question have to be awarded differently. This overrides both the quiz-wide ScoreStrategy and the difficulty-wide DifficultyStrategy. There can be one instance per question.
While evaluating the responses of the quiz, I want to implement a level-by-level fallback mechanism:
For each question:
Check if there is a QuestionScoreStrategy for the question and return the strategy if one is found.
If not, fallback to DifficultyScoreStrategy and check if there is a strategy for the difficulty level of the question being evaluated
and return it if a strategy is found.
If not, fallback to the quiz-wide ScoreStrategy and check if one exists and return it if it does,
If there is no ScoreStrategy either, use default as { Correct = 1, Incorrect = 0, Unattempted = 0 }(It would be great if I can make this configurable as well, something much like the .NET's elegant way:
options => {
options.UseFallbackStrategy(
correct: 1,
incorrect: 0,
unattempted: 0
);
}
).
Summary
I've summarized the above info in a table:
Strategy Type
Priority
Maximum instances per quiz
QuestionScoreStrategy
1st (highest)
As many as there are questions in the quiz
DifficultyScoreStrategy
2nd
4, one for each difficulty level
ScoreStrategy
3rd
Only one
Fallback strategy (Default { Correct = 1, Incorrect = 0, Unattempted = 0})
4th (lowest)
Configured for the entire app. Shared by all quizzes
I have a container class called EvaluationStrategy that holds these score strategies among other evaluation info:
partial class EvaluationStrategy
{
public int Id { get; set; }
public int QuizId { get; set; }
public decimal MaxScore { get; set; }
public decimal PassingScore { get; get; }
public IEnumerable<ScoreStrategy> ScoreStrategies { get; set; }
}
What I have tried:
I have added a method called GetStrategyByQuestion() to the same EvaluationStrategy class above(note it is declared as partial) that implements this fallback behavior and also a companion indexer that in turn calls this method. I have declared two HashSets of types DifficultyScoreStrategy and QuestionScoreStrategy and an Initialize() method instantiates them. All the score strategies are then switched by type and added to the appropriate HashSet, there can only be one ScoreStrategy per quiz, which will be stored in defaultStrategy:
partial class EvaluationStrategy
{
private ScoreStrategy FallbackStrategy = new() { Correct = 1, Incorrect = 0, Unattempted = 0 };
private ScoreStrategy defaultStrategy;
HashSet<DifficultyScoreStrategy> dStrategies;
HashSet<QuestionScoreStrategy> qStrategies;
public void Initialize()
{
qStrategies = new();
dStrategies = new();
// Group strategies by type
foreach (var strategy in strategies)
{
switch (strategy)
{
case QuestionScoreStrategy qs: qStrategies.Add(qs); break;
case DifficultyScoreStrategy ds: dStrategies.Add(ds); break;
case ScoreStrategy s: defaultStrategy = s; break;
}
}
}
public ScoreStrategy this[Question question] => GetStrategyByQuestion(question);
public ScoreStrategy GetStrategyByQuestion(Question question)
{
if (qStrategies is null || dStrategies is null)
{
Initialize();
}
// Check if question strategy exists
if (qStrategies.FirstOrDefault(str => str.Question.Id == question.Id) is not null and var qs)
{
return qs;
}
// Check if difficulty strategy exists
if (dStrategies.FirstOrDefault(str => str.Question.Difficulty == question.Difficulty) is not null and var ds)
{
return ds;
}
// Check if default strategy exists
if (defaultStrategy is not null)
{
return defaultStrategy;
}
// Fallback
return FallbackStrategy;
}
}
This method seems a bit clumsy and doesn't quite feel right to me. Using a partial class and adding to EvalutationStrategy doesn't seem right either. How do I implement this level-by-level fallback behavior? Is there a design pattern/principle I can use here? I know many things in the .NET framework fallback to default conventions if not configured. I need something similar. Or can someone simply recommend a cleaner and elegant solution with better maintainability?
NOTE/ADDITIONAL INFO: The ScoreStrategys and EvaluationStrategy for all quizzes are stored in a database managed by EF Core(.NET 5) with TPH mapping:
modelBuilder.Entity<ScoreStrategy>()
.ToTable("ScoreStrategy")
.HasDiscriminator<int>("StrategyType")
.HasValue<ScoreStrategy>(0)
.HasValue<DifficultyScoreStrategy>(1)
.HasValue<QuestionScoreStrategy>(2)
;
modelBuilder.Entity<EvaluationStrategy>().ToTable("EvaluationStrategy");
I have a single base DbSet<ScoreStrategy> ScoreStrategies and another DbSet<EvaluationStrategy> EvaluationStrategies. Since EvaluationStrategy is an EF Core class, I'm a bit skeptical about adding logic(GetStrategyByQuestion()) to it as well.
With Polly
There is a 3rd party library called Polly which defines a policy called Fallback.
With this approach you can easily define a fallback chain like this:
public ScoreStrategy GetStrategyByQuestionWithPolly(Question question)
{
Func<ScoreStrategy, bool> notFound = strategy => strategy is null;
var lastFallback = Policy<ScoreStrategy>
.HandleResult(notFound)
.Fallback(FallbackStrategy);
var defaultFallback = Policy<ScoreStrategy>
.HandleResult(notFound)
.Fallback(defaultStrategy);
var difficultyFallback = Policy<ScoreStrategy>
.HandleResult(notFound)
.Fallback(() => GetApplicableDifficultyScoreStrategy(question));
var fallbackChain = Policy.Wrap(lastFallback, defaultFallback, difficultyFallback);
fallbackChain.Execute(() => GetApplicableQuestionScoreStrategy(question));
}
I've extracted the strategy selection logic for QuestionScoreStrategy and DifficultyScoreStrategy like this:
private ScoreStrategy GetApplicableQuestionScoreStrategy(Question question)
=> qStrategies.FirstOrDefault(str => str.Question.Id == question.Id);
private ScoreStrategy GetApplicableDifficultyScoreStrategy(Question question)
=> dStrategies.FirstOrDefault(str => str.Difficulty == question.Difficulty);
Pros
There is a single return statement
The policy declarations are separated from chaining
Each and every fallback can be triggered by different conditions
Primary selection logic is separated from the fallbacks
Cons
The code is really repetitive
You can't create a fallback chain by utilizing a fluent API
You need to use a 3rd party library
Without Polly
If you don't want to use a 3rd party library just to define and use a fallback chain you do something like this:
public ScoreStrategy GetStrategyBasedOnQuestion(Question question)
{
var fallbackChain = new List<Func<ScoreStrategy>>
{
() => GetApplicableQuestionScoreStrategy(question),
() => GetApplicableDifficultyScoreStrategy(question),
() => defaultStrategy,
() => FallbackStrategy
};
ScoreStrategy selectedStrategy = null;
foreach (var strategySelector in fallbackChain)
{
selectedStrategy = strategySelector();
if (selectedStrategy is not null)
break;
}
return selectedStrategy;
}
Pros
There is a single return statement
The fallback chain declaration and evaluation are separated
It is simple and concise
Cons
It is less flexible: each fallback selection is triggered by the same condition
Primary selection is not separated from fallbacks
You can sort the sequence of ScoringMethods by your priority.
First you sort by whether str is QuestionScoreStrategy and str.Question.Id == question.Id.
Then you sort by whether str is DifficultyScoreStrategy and str.Question.Difficulty == question.Difficulty.
(Note that since false comes before true, you'll have to invert the conditions)
Then you can just do FirstOrDefault() ?? defaultStrategy.
Example:
var defaultStrategy = new() { Correct = 1, Incorrect = 0, Unattempted = 0 };
var selectedStrategy = Strategies.OrderBy(str =>
!(str is QuestionScoreStrategy questionStrat && questionStrat.Question.Id == question.Id)
).ThenBy(str =>
!(str is DifficultyScoreStrategy difficultySrat && difficultySrat.Difficulty == question.Difficulty)
).FirstOrDefault() ?? defaultStrategy;
You can easily add more "levels" to this by adding more ThenBy clauses.
I imagine that all data (questions, strategies, quizes is stored in database). Then I would expect such ways of getting each strategy:
Question strategy
var questionStrategy = dbContext.ScoreStrategies.SingleOrDefault(ss => ss.QuesionId == question.Id);
Difficulty strategy:
var difficultyStrategy = dbContext.ScoreStrategies.SingleOrDefault(ss => ss.Difficulty == question.Difficulty);
Default strategy for quiz:
var quizStrategy = dbContext.ScoreStrategies.SingleOrDefault(ss => ss.QuizId == question.QuizId)
Building on this and what you already provided, strategy is just three numbers: points for correct answer, points for incorrect and unattempted answer.
So this makes perfect candidate for abstract class, which would serve for base class for three entities - three types of strategy - those will be three tables, because each has different relations:
public abstract class ScoreStrategy
{
public double Correct { get; set; }
public double Incorrect { get; set; }
public double Unattempted { get; set; }
}
// Table with FK relation to Questions table
public class QuestionScoreStrategy : ScoreStrategy
{
public Question { get; set; }
public int QuestionId { get; set; }
}
// If you have table with difficulties, there should be FK relation to it.
// If you do not have table - it's worth consideration, you could then
// easily add more difficulties.
public class DifficultyStrategy : ScoreStrategy
{
public QuestionDifficulty Difficulty { get; set; }
}
// FK relation to Quizes table
public class QuizScoreStrategy : ScoreStrategy
{
public Quiz { get; set; }
public int QuizId { get; set; }
}
This way you end up with well grained tables that stores only relevant data.
Then, usage would become:
// Ideally, this method should be in some repoistory (look at repository design pattern) in data access layer
// and should leverage usage of async / await as well.
public ScoreStrategy GetScoreStrategy(Question question)
{
return dbContext.QuestionScoreStrategies.SingleOrDefault(qs => qs.QuestionId == question.Id)
?? dbContext.DifficultyStrategies.SingleOrDefault(ds => ds.Difficulty == question.Difficulty)
?? dbContext.QuizScoreStrategies.SingleOrDefault(qs => qs.QuizId == question.QuizId);
}
Then you could use this method in such way:
// This should be outside data access layer. Here you perform logic of getting question.
// This could be some ScoringManager class which should be singleton (one instance only).
// Then you could define fallback in private fields:
private readonly double FALLBACK_CORRECT_SCORE;
private readonly double FALLBACK_INCORRECT_SCORE;
private readonly double FALLBACK_UNATTEMPTED_SCORE;
// private constructor, as this should be singleton
private ScoringManager(double correctScore, double incorrectScore, double unattemptedScore)
=> (FALLBACK_CORRECT_SCORE, FALLBACK_INCORRECT_SCORE, FALLBACK_UNATTEMPTED_SCORE) =
(correctScore, incorrectScore, unattemptedScore);
public double CalcScoreForQuestion(Question question)
{
var scoreStrategy = GetScoreStrategy(question);
if (question answered correctly)
return scoreStrategy?.Correct ?? FALLBACK_CORRECT_SCORE;
if (question answered incorrectly)
return scoreStrategy?.Incorrect ?? FALLBACK_INCORRECT_SCORE;
if (question unattempted)
return scoreStrategy?.Unattempted ?? FALLBACK_UNATTEMPTED_SCORE;
}
NOTE
This is just the draft how I would organize things and most probably when writing code I would come up with improvements, but I think this is direction to go. For example ScoringManager could have ConfigureFallbackScore method, which would allow dynamically changing fallback scores (this would require making respective fields not readonly).
UPDATE
Define fallback strategy, in order to do that define enum:
public enum FallbackLevel
{
Difficulty,
Question,
Quiz,
}
Then scoring manager could have method to configure strategy (together with backing fields):
private FallbackLevel _highPrecedence;
private FallbackLevel _mediumPrecedence;
private FallbackLevel _lowPrecedence;
public void ConfigureFallbackStrategy(FallbackLevel highPrecedence, FallbackLevel mediumPrecedence, FallbackLevel lowPrecedence)
{
_highPrecedence = highPrecedence;
_mediumPrecedence = mediumPrecedence;
_lowPrecedence = lowPrecedence;
}
Then we would write getting strategy logic in manager:
public ScoreStrategy GetScoreStrategy(Question question)
{
var scoreStrategy = GetScoreStrategy(_highPrecedence, question)
?? GetScoreStrategy(_mediumPrecedence, question)
?? GetScoreStrategy(_lowPrecedence, question);
}
private ScoreStrategy GetScoreStrategy(FallbackLevel lvl, Question question) => lvl switch
{
FallbackLevel.Difficulty => dbContext.DifficultyStrategies.SingleOrDefault(ds => ds.Difficulty == question.Difficulty),
FallbackLevel.Question => dbContext.QuestionScoreStrategies.SingleOrDefault(qs => qs.QuestionId == question.Id),
FallbackLevel.Quiz => dbContext.QuizScoreStrategies.SingleOrDefault(qs => qs.QuizId == question.QuizId),
}
This way it is super easy to configure fallback strategy any way you want. Of course, there are some considerations still:
make sure that all fallback strategies are unique, so for example it is impossible to have high, medium and low startegy the same,
db context should be accessed only via repository pattern
add some more sanity checks (like nulls etc.)
I omitted those parts, as I focused on sheer functionality.
I'm currently building a test application that manages parts for engineers, and I've hit a snag. I have a few different classes, including PartsModel and EngineerModel, and I want to update a list of parts that an engineer has, but I'm mindful of issues from either transposed parameters or from structuring the code in a way that unnecessarily couples to a particular class.
The two classes, with some relevant properties:
public class PartModel
{
public int PartId { get; private set; }
public string PartTitle { get; set; }
public string PartDescription { get; set; }
public int Quantity { get; set; }
public int MinimumStock { get; set; }
public void AddToStock (int quantityToAdd) {
Quantity += quantityToAdd;
}
public void RemoveFromStock (int quantityToRemove) {
Quantity -= quantityToRemove;
CheckMinimumStock();
}
}
public class EngineerModel
{
public int EngineerId { get; private set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<PartModel> PartsInStock { get; set; } = Factory.CreatePartsList();
}
As you can see, each engineer has a list of parts they have in stock via a List<PartModel>. I want to pass another list to this one so that I can update it respectively (incrementing or decrementing quantities, and then adding or removing parts to the list as necessary).
The first warning bell is that it takes two inputs of the same type, and is going to fill one from the other one (which isn't needed afterwards), so you're essentially modifying one input and destroying the other. To me, this presents a danger of the inputs getting transposed and the wrong list being either returned or updated (depending on whether it returns or just acts on the list). Because it removes items that have no quantity, it can't check the list length and just update the longer one, because there are possible cases where the engineer's list is shorter (maybe they're a new engineer, or maybe they just had a large shipment of parts sent when they were running low on stock). If it did just keep parts with quantity zero, then you're threatening scalability of both engineers and parts (not to mention any other objects that use the same operation).
So, put it as a method in the EngineerModel class and operate on PartsInStock, right? But what about when I want to use the same operation on other classes (e.g. if I have a list of parts associated to a work task)? Then I extract the method out to another class and... I'm passing the two lists as parameters in the method, so I'm back to where I was.
Am I being reasonable in not wanting to have two parameters of the same type, and how do I structure the code to deal with this, but without creating unnecessary coupling? If I'm not being reasonable, what am I overlooking?
Use an extension method
Thanks to #DavidBrowne-Microsoft for clarifying this. By defining an extension method for List<PartModel>, it only needs the one parameter - the list containing the updates (foreach below based on #Valentin's answer to this question).
public static class PartsHandler
{
public static List<PartModel> UpdateStockQuantitiesWith(this List<PartModel> stockToBeUpdated, List<PartModel> stockUpdates) {
foreach ( var part in stockUpdates )
{
var partToBeUpdated = stockToBeUpdated.FirstOrDefault(x => x.PartId == part.PartId);
if ( partToBeUpdated != null )
{ partToBeUpdated.Quantity += part.Quantity; }
else
{ stockToBeUpdated.Add(part); }
}
stockToBeUpdated.RemoveAll(x => x.Quantity <= 0);
return stockToBeUpdated;
}
}
Now any class that needs to implement this can simply call it in a method on the respective property. For example, in the EngineerModel class, it can operate on the PartsInStock property:
public void AddPartsToStock(List<PartModel> partsSent) {
PartsInStock.UpdateStockQuantitiesWith(partsSent);
}
I've been looking into rules engines and such, but I really am not sure where to start. This is more for experimentation, but I'd like to implement something like this for work in the future. Basically, I have an application where a user submits a form and populates a POCO object with several properties. I want the administrator of the application to be able to define rules based on the properties of said object and store them in a relational database. When the form is submitted, I would then make a decision based on the user defined rules. For example, the admin can go into the application and define rules like following:
if (typeID == 4 && request.benchMarkScore < 10) {
request.denied = true;
request.denyReasons.Add("Score too low for this product");
}
Here's my POCO Object example:
class Request
{
public int benchMarkScore { get; set; }
public int typeID { get; set; }
public double rate { get; set; }
public bool isEligable { get; set; }
public bool denied { get; set; }
public List<string> denyReasons { get; set; }
public Dictionary<string, double> adjustments;
}
Granted I know this is an overly simplified example, but I come across many situations where I users could benefit from this functionality in my applications. I'm not looking for a complete solution, but instead an idea of where to start.
There are a number of ways you could go about this. One suggestion would be to leverage reflection itself, and allow admins to apply a rule. I'm going to keep this simple, but a rule would consist of:
A bunch of properties, operands, and values
The reason(s) for denial.
So let's define that. I am going to keep this simple and just handle equality, you can define additional ones:
public enum Operand
{
Equals
}
Now, we can define an interface called IRule. I am defining an interface so that in the future, you could potentially put special, more complicated, rules in.
public interface IRule<TPOCO> where TPOCO : class
{
bool IsValid(TPOCO poco);
}
And now we'll define our Rule class (Note: this doesn't handle indexed properties):
public class PropertyCompareRule : IRule<Request>
{
private sealed class PropertyCompare
{
public string PropertyName {get; set; }
public Operand Operand {get; set; }
public object Value {get; set;}
public string Reason {get; set; }
}
private List<PropertyCompare> _comparers = new List<PropertyCompare>();
public bool IsValid(Request poco)
{
bool isValid = true; // let's be optimistic!
PropertyInfo[] properties = poco.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where((property) => property.GetIndexParameters().Length == 0 && property.CanRead).ToArray();
foreach(var property in properties)
{
foreach(var comparer in _comparers)
{
bool localIsValid;
if(comparer.PropertyName == property.Name)
{
object val = property.GetValue(poco, null);
switch(comparer.Operand)
{
case Operand.Equals:
{
localIsValid = object.Equals(val, property.Value);
break;
}
}
if(!localIsValid)
{
poco.denyReasons.Add(comparer.Reason);
isValid = false;
}
}
}
}
return isValid;
}
public void AddComparer(string propertyName, Operand op, object value, string reason)
{
_comparers.Add(new PropertyCompare() { PropertyName = propertyName, Operand = op, Value = value, Reason = reason });
}
}
It wouldn't be difficult for you to be able to persist the property name, operand, and value details in a database or other such storage. Assuming we fleshed out our enum above, we could conceivably do:
PropertyCompareRule rule = new PropertyCompareRule();
rule.AddComparer("typeID", Operand.Equal, 4, "Reason 1");
rule.AddComparer("benchMarkScore", Operand.LessThan, 10, "Reason 2");
bool valid = rule.IsValid(somePocoInstance);
Edit: Some notes
I use a localIsValid rather than bailing out at the first opportunity. You can change this if you want, but the idea is that it allows a single rule to have multiple points of deniability. This may or may not be what you wish - but it's easy enough to refactor the code so that it bails out the moment a single property comparison fails.
This is a nit-pick, but generally C# style-guidlines dictate properties shouldn't be camel-caps... but that's entirely up to you at the end of the day :)
As I understand you, you are looking for some kind of a scripting system for business rules. I found this blog post where some scripting environment are mentioned.
You can also create assemblies on the fly like mentioned here: https://stackoverflow.com/a/4181855/1229622.
I need help as to how do I go about the structure of classes. How do I use Indexers? I want to have something like
Company.Employees[empId].Employee["Designation"].Salary
To be more specific something like
Grid.Rows[rowIndex].Columns["CurrentColumnName"].Width
Add a method like
public string this[string s]
{
get{
if(s == ...)
return this.property;
}
}
Yet, this seems to be more a Situation for Collections, but
see here for a complete example.
Actually indexers are used to get element by index, and your EmpId is not a good candidate for indexing as these may be compost or non sequential.
If you still want to use it here is the code. It will mimic as Indexer but its modified version.
class Employee
{
public int EmpId { get; set; }
public float Salary { get; set; }
public string Designation { get; set; }
}
class Employees
{
List<Employee> EmpList = new List<Employee>();
public Employee this[int empId]
{
get
{
return EmpList.Find(x => x.EmpId == empId);
}
}
}
I would rather have a method because I can make it generic.
public T GetPropertyValue<T>(string property)
{
var propertyInfo = GetType().GetProperty(property);
return (T)propertyInfo.GetValue(this, null);
}
var emp = employee.GetPropertyValue<Employee>("Designation");
var salary = emp.Salary;
That said... Be careful for having so many dot notations. When you get that NullReferenceException on your line in a log file, it is very difficult to find out what exactly was null. So rather break things up a bit and have more lines then you have less trouble of resolving bugs.
I am trying to build a data structure that can store the trial by trail results of a group of different tests I am running. The test all consist of a number of trails but some of the information that I want to save and later use is different for the different tests. For example, the results of TestA might look like:
Trial(int) WasResponseCorrect(bool) WhichButtonWasPressed(string) SimulusLevel(double)
1 false "Up" 3.5
2 true "Left" 6.5
Where TestB might have different types of result fields:
Trial(int) WasResponseCorrect(bool) ColorPresented(string) LetterPresented(char) LetterGuessed(Char)
1 false green G C
2 false blue H F
I was thinking of creating a dictionary with the field names as the keys (ex. WasResponseCorrect) and an array of the field values as the values of dic. I can't figure out how to do that. Maybe there is a better way to store the information but I can't think of how to do it. I am working with .net (VB and C#) but I think I can understand and convert most any code if you know of examples in other languages. Thanks!
Without knowing more about your requirements (how you are going to store the data, for example), it seems like polymorphism is what you're looking for. That is, you have a superclass (called Trial) and subclasses that represent the specific trial types. For example:
public class Trial {
public int Id { get; set; }
public bool WasResponseCorrect { get; set; } // if this is in every type of trial
// anything else that is common to ALL trial types
}
public class TrialA : Trial {
public string WhichButtonWasPressed { get; set; }
public double SimulusLevel { get; set; }
}
public class TrialB : Trial {
public string ColorPresented { get; set; }
public char LetterPresented { get; set; }
public char LetterGuessed { get; set; }
}
That way you can have a list of Trial objects, but the actual runtime type of those objects can be TrialA or TrialB.