0

I've created a simple DateTime array that contains 3 items. These items are set to use the values of three different DateTimePickers on my form. Before I go further into using the array, I need to make sure it is actually using the correct values, and it does not appear to be doing so. Here's my code:

namespace Test
{
    public partial class Form1 : Form
    {
        DateTime[] monSchedule = new DateTime[3];

        public Form1()
        {
            InitializeComponent();

            monSchedule[0] = monStart.Value;
            monSchedule[1] = monEnd.Value;
            monSchedule[2] = monLunch.Value;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            setDefaults();
        }

        private void setDefaults()
        {
            monStart.Value = DateTime.Parse("00:00");
            monEnd.Value = DateTime.Parse("00:00");
            monLunch.Value = DateTime.Parse("00:00");
        }

        private void validate()
        {
            MessageBox.Show("You entered time " + monSchedule[0]);
        }

When I load my form, setDefaults(); should change the values to the current date with a time of 00:00. When I press the button to show the value in the array, it is pulling current date and current time. I need it to pull whatever the current time in that DateTimePicker is. So if a user types 10:00 into the DateTimePicker (they are formatted HH:mm), then I need the MessageBox to say the time is 10:00 AM. If I change the value to 22:00, then I need the messagebox to say the time is 10:00 PM. etc. (Date is irrelevant in my scenario, I'm not concerned with what the date is at all. Only the time.)

I suspect it may be because of the order it's written in. Is the array storing the value of the DateTimePicker BEFORE setDefaults(); is run? If so, how do I make the values of the array items dynamic since the values of the DateTimePickers are going to change a lot and I need the array elements to be updating with the latest values?

EXTRA INFO:
-Using Visual Studio
-Added the DateTimePickers in design view, changed the format to HH:mm there, did not change the default values in design view
-Ignoring date completely, only concerned with time right now

PS: I was also struggling with where to declare the array so it was accessible in multiple other methods and found I had to declare the array initializer within public partial class Form1, but then add the items in the array within public Form1(), because it wouldn't let me add them under public partial class Form1. I don't know if this is correct though, but it seemed to work when I tested with an array of strings so I went with it.

  • In your _setDefaults()_ you set the values in the 3 dtp, you are not changing the values in the array. Instead the array is initialize in the constructor (before the Form_Load event) with the current value of the DateTimePicker. This default value is the current date and time. – Steve Dec 27 '18 at 22:43
  • I can't quite follow your code (since it's incomplete). Where do you initialize `monStart`, `monEnd` and `monLunch`. Whatever you put in those values before you construct an instance of `Form1` will be what you put in the array. The other thing you need to remember is that DateTime is a _Value Type_, instances of `DateTime` get copied by value (like an `int` or `float`) and not by reference. Once you stick something in the array, if you change what you copied from, it doesn't change what's in the array – Flydog57 Dec 27 '18 at 22:45
  • @flydog57 They were added in Visual Studio designer, so I guess Visual Studio did all of that initializing. I'm sorry for the confusion, my previous posts were not well received when I included extra information like that, so I just stripped it down this time. – irisshootsface Dec 27 '18 at 22:51
  • 1
    Set the DTPs Value (`monStart.Value = DateTime.Parse("00:00");` etc.) in the Form.Load event. Add an Event Handler (the same) to the [ValueChanged](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.datetimepicker.valuechanged) event of the DTP controls. Use the `sender` object to determine which DTP raised the event. Set the value of the DTP to the corresponding array index in the event handler. Show a MessageBox if needed. Your `monSchedule` array is a field, accessible anywhere in the Form1 class. – Jimi Dec 27 '18 at 22:58
  • @Jimi I'll have to look into this sender object. I already set the DTPs value in the Form.Load event via the `setDefaults()` function that you see called. I'm familiar with using the ValueChanged events because that is a HUGE portion of my overall code that this array is going to get incorporated into. But the `sender` object is new to me. If you have time, could you link a good resource on using it? – irisshootsface Dec 27 '18 at 23:06
  • When you prepare a [mcve], what you want to do is include all the information that is relevant, and toss out the rest. For example, you can pull the designer code out of the `.designer.cs` file into you own (do this on a copy, you'll break the designer when you do this), then make sure it still compiles. – Flydog57 Dec 27 '18 at 23:10
  • @Flydog57 Thank you. I've just never actually touched the `designer.cs` file as I've been taught only how to use designer to create things so far. Probably a bad way to start off learning but that was what was given to me so I ran with it. This snippit of code is just me trying to get an understanding of arrays, specifically an array with DateTime values, before incorporating it into my main project. – irisshootsface Dec 27 '18 at 23:19
  • *"When I press the button to show the value in the array, it is pulling current date and current time."* Then perhaps you should show *that* code... – Rufus L Dec 27 '18 at 23:52

3 Answers3

0

It is unclear what you want the date component of the date part of DateTime to be DateTime.Parse("00:00") should return midnight today or 12/27/18 12:00:00 AM; This is also the same value as DateTime.Today In addition, you can create a new DateTime with a constructor

monStart.Value = new DateTime(2018, 12, 27, 0, 0, 0);

This is midnight the today

Paul Hebert
  • 296
  • 3
  • 8
  • I didn't include a lot of other details that may be relevant here.. Date is completely irrelevant. We're working only on times. But it's going to store a date value anyway so I just ignore it. I need the array to store the most CURRENT value - be that changed by a method in the program (like setDefaults) or changed by the user. For example, if I type 10:00 into the DateTimePicker, I want MessageBox to show 10:00 in the time. I'll edit my post with this clarification. – irisshootsface Dec 27 '18 at 22:46
  • Instead of using DateTime use a TimeSpan. You could then just store time. Time span starts at 0:0:0 https://learn.microsoft.com/en-us/dotnet/api/system.timespan.parse?view=netframework-4.7.2#System_TimeSpan_Parse_System_String_ – Paul Hebert Dec 28 '18 at 00:10
0

Note:
Reading the description in your updated question, it appears that the DateTimePicker controls values are accessed on a Button Click. If this is the actual scenario, you probably don't need a DateTime array field at all: you could just read the values directly from the DTP controls and use the values in-place.
The example assumes (to comply with the question) that you need that array anyway.


A possible way to proceed:

  • Set the default values in the Form.Load event. Initialize the monSchedule array values right after, so the values are synchronized. Note that the Form.Load event handler code is (of course) executed after the class constructor (public Form1() { }): the Form object must be already initialized.
  • Assign an event handler to all the DateTimePicker controls (same event for all). The event handler is used to assign the new values to the monSchedule array. The event could be the ValueChanged event or, possibly, the more generic Validating event. The former is raised each time you change any part of the Time value (the hour value or minutes value). The latter only when the control loses the focus. Your choice.
  • Use the sender object in the event handler to determine which control raised the event and update the corresponding array value.

An example, using a switch statement and a case statement with a when clause:

Notes:
1. You need C# 7.0+ to use this switch syntax. Otherwise, you could switch using a Type pattern (see the Docs) or the DateTimePicker name (see the example).

2. The DTP_ValueChanged(object sender, EventArgs e) event (the ValueChanged handler) is assigned to all the DateTimePicker controls.


public partial class Form1 : Form
{
    DateTime[] monSchedule = new DateTime[3];

    private void Form1_Load(object sender, EventArgs e)
    {
        SetDefaultDTPValues();
    }

    private void SetDefaultDTPValues()
    {
        monStart.Value = DateTime.Parse("00:00");
        monEnd.Value = DateTime.Parse("00:00");
        monLunch.Value = DateTime.Parse("00:00");

        monSchedule[0] = monStart.Value;
        monSchedule[1] = monEnd.Value;
        monSchedule[2] = monLunch.Value;
    }

    private void DTP_ValueChanged(object sender, EventArgs e)
    {
        switch (sender)
        {
            case DateTimePicker dtp when dtp.Equals(monStart):
                monSchedule[0] = dtp.Value;
                break;
            case DateTimePicker dtp when dtp.Equals(monEnd):
                monSchedule[1] = dtp.Value;
                break;
            case DateTimePicker dtp when dtp.Equals(monLunch):
                monSchedule[2] = dtp.Value;
                break;
        }
    }
}

On a Button.Click event:

private void button1_Click(object sender, EventArgs e)
{
    MessageBox.Show($"Start: {monSchedule[0].ToString("hh:mm tt")}  " +
                    $"End: {monSchedule[1].ToString("hh:mm tt")}  " +
                    $"Lunch: {monSchedule[2].ToString("hh:mm tt")}");
}

If the C# version in use doesn't allow this switch statement syntax, you can use the DateTimePicker name instead (there are other options, see the examples in the Docs):

    private void DTP_ValueChanged(object sender, EventArgs e)
    {
        DateTimePicker dtp = sender as DateTimePicker;
        switch (dtp.Name)
        {
            case "monStart":
                monSchedule[0] = dtp.Value;
                break;
            case "monEnd":
                monSchedule[1] = dtp.Value;
                break;
            case "monLunch":
                monSchedule[2] = dtp.Value;
                break;
        }
    }
Jimi
  • 29,621
  • 8
  • 43
  • 61
0

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.

  1. 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.

  2. You have the aforementioned controls times seven, one set for each day of the week.

  3. 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.

  4. 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.

  1. 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?

  1. 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.

  2. 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?"

  3. 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.

madreflection
  • 4,744
  • 3
  • 19
  • 29