I have to say that this is a bit of a regression. In your previous question, JoshPart gave you good advice in the form of user controls, although he may have left some gaps too large for you to fill on your own.
Using arrays in this manner might work for a single day, but it won't scale well to a full week.
In case anyone reading this is wondering why I'm talking about a full week, I refer you to the previous question. Also, I recognize that I'm going off-topic for this specific question but I believe this to be an XY problem and the previous question was actually based on the real problem and work that was more on-the-mark.
Let's start with what we know. I've gleaned this from the two questions and the various comments in both.
You have DateTimePicker
controls for start, end, and lunch. You're only interested in the time portion so you have Format
set to "Custom" and CustomFormat
set to "HH:mm". Assumption: lunch is a fixed length so the end time isn't needed.
You have the aforementioned controls times seven, one set for each day of the week.
You've written validation code (range tests) to determine if values are entered correctly, and you're able to show a label with red exclamation marks when that test fails.
You've identified that it's getting too complicated just having a bunch of controls on a form.
So far, so good. Now for your goal.
- You're looking for a way to organize the controls, and the data they collect, to make it easier to work with them.
A user control is still the way to go here. You'll benefit from encapsulating all that repeated functionality into a single place and being able to reuse it.
Start by creating a user control -- we'll call it DayPanel
-- and put all the controls for a single day on that canvas. Name the controls without any regard for the day of week (e.g. start
, lunch
, and end
). Your user control will neither know nor care which day it represents.
Add an event handler for the ValueChanged
event to the DateTimePicker controls. Instead of double-clicking the control, go to the events list in the Properties tool window and type a name, such as the one below, for the ValueChanged
event. Do the same for the other two controls and it will reuse the event handler that it created the first time. Whenever the user changes a time, this event handler will be called and it will effect changes to the UI.
private void picker_ValueChanged(object sender, EventArgs e)
{
// In case you need to know which DateTimePicker was changed, take a look at 'sender':
//DateTimePicker picker = (DateTimePicker)sender;
UpdateWarningState();
}
As Jimi mentioned, the sender
object will be a reference to the DateTimePicker control that sent the event. You probably won't need it but it's there if you do.
UpdateWarningState
just hides/shows the warning label based on the validity of the inputs.
private void UpdateWarningState()
{
warningLabel.Visible = !IsInputValid(start.Value.TimeOfDay, lunch.Value.TimeOfDay, end.Value.TimeOfDay);
}
I had suggested in comments on the previous question that it seemed to make sense to get true
if the inputs are valid and then use the logical negative for the visibility of the warning label.
As Paul Hebert pointed out, you really only need to compare a TimeSpan
, so IsInputValid
receives the TimeOfDay
property to deal with only that much.
private bool IsInputValid(TimeSpan startTime, TimeSpan lunchTime, TimeSpan endTime)
{
return startTime < lunchTime && lunchTime.Add(TimeSpan.FromMinutes(30)) < endTime;
}
In fact, even though you're only inputting a time, the control still returns a date part in its Value
property. If you want to be certain that you're not comparing times in different dates, you'll definitely need to use the TimeOfDay
property. That said, by not presenting the date part, you have a measure of control over that so it's not a pressing concern. If you had to worry about crossing over midnight, that would complicate things.
Note that I've dealt with that earlier assumption that lunch is a fixed length by adding 30 minutes in the comparison to the end time.
Why not just do all that in the ValueChanged
event handler?
The Single Responsibility Principle. IsInputValid
does one thing: business logic; it tells you if the inputs are valid based on range testing. UpdateWarningState
does a different thing: UI logic; it updates the visibility of warning label based on the validity of the inputs.
UpdateWarningState
is reusable. You can call it from other event handlers in the future. Event handlers really shouldn't ever do much. They're more like telephone operators: "how may I direct your call?"
IsInputValid
is reusable. The business logic can be extracted from your UI code at some point in the future and be reused by something else. I'll admit that the name leaves something to be desired; it fits here but probably should be different outside this context.
But what good is this user control if you have no way of working with its data? The consumer needs to be able to interact with it. A user control is just another class so you can define public properties, methods, and events as you see fit. We'll add properties for the three values of interest:
public TimeSpan Start
{
get => start.Value.TimeOfDay;
set => start.Value = start.Value.Date + value;
}
public TimeSpan Lunch
{
get => lunch.Value.TimeOfDay;
set => lunch.Value = lunch.Value.Date + value;
}
public TimeSpan End
{
get => end.Value.TimeOfDay;
set => end.Value = end.Value.Date + value;
}
What's interesting to note about these properties is that they don't have their own backing storage. Instead, they defer to the controls and translate between their own TimeSpan
data type and the controls' DateTime
data type. On get
, they return just the TimeOfDay
property. On set
, they remove the time portion (with .Date
) and add the time of day.
If you were building this for someone else to consume, you'd want to ensure that the Days
property is 0
and that the whole value is non-negative, and either throw ArgumentOutOfRangeException
or (gasp!) clamp the value to the acceptable range.
Now that you have a functioning control for a single day, you can slap a bunch of them on the main form. Back in Form1
, add seven instances of the DayPanel
control and name them monday
through sunday
. Before we get to initialization, let's create a lookup for these user controls.
private readonly Dictionary<DayOfWeek, DayPanel> _dayPanelLookup;
public Form1()
{
InitializeComponent();
_dayPanelLookup = new Dictionary<DayOfWeek, DayPanel>()
{
[DayOfWeek.Monday] = monday,
[DayOfWeek.Tuesday] = tuesday,
[DayOfWeek.Wednesday] = wednesday,
[DayOfWeek.Thursday] = thursday,
[DayOfWeek.Friday] = friday,
[DayOfWeek.Saturday] = saturday,
[DayOfWeek.Sunday] = sunday
};
}
Now the Load
handler can then initialize all the properties. This DefaultTime
duplicates the TimeSpan.Zero
constant for the purpose of giving it a distinct meaning and can help with refactoring later on.
private static readonly TimeSpan DefaultTime = TimeSpan.Zero;
private void Form1_Load(object sender, EventArgs e)
{
SetDefaults();
}
private void SetDefaults()
{
foreach (DayPanel dayPanel in _dayPanelLookup.Values)
{
dayPanel.Start = DefaultTime;
dayPanel.Lunch = DefaultTime;
dayPanel.End = DefaultTime;
}
}
And just for fun, we can use _dayPanelLookup
to grab one of them based on a variable containing the day of the week.
public void someButton_Click(object sender,
{
DayOfWeek whichDay = SelectADay();
DayPanel dayPanel = _dayPanelLookup[whichDay];
// ...
}
That should address the main concern of organizing the controls and making it easy to work with them and their values. What you do with it once the user presses some as-yet-unidentified button on the form is a whole new adventure.
There are yet better ways of doing all of this, I'm sure. I'm not a UI developer, I just play one on TV. For your purposes, I hope this not only gives you the guidance you needed at this point in this project but also illuminates new avenues of thought about how to structure your programs in the future.