3

I have a system which manages Vehicles and Staff, when you click on their name based on a date you should be able to see the times that they are available on that day.

It will only show 1 day based on the date chosen on the previous form! So I need 1 column but times could be 12:30-14:15 etc

Something visual like this:

Visual Time Visual Time

Picture:

enter image description here

I have looked in to creating a custom control or user control but my knowledge on the subject is low and I've spent a few hours running around in a circle.

Hamlet Hakobyan
  • 32,965
  • 6
  • 52
  • 68
  • 2
    Do you have a specific question? Nobody will implement this for you. You could at least show what you've tried. – Nico Schertler Sep 24 '13 at 11:01
  • I'm looking for something similar or a point in the right direction, not looking for someone to code it! No idea where to start so do not have any code, currently I can output a text box with the times booked, START: END: etc for that day, but need it visually. Any similar projects/ideas would be helpful, I tried to use a grid and colouring but the 1/2 or 1/4 hours don't work. Thanks – James Smithy Cleave Sep 24 '13 at 11:03
  • It all depends on your SQL fetching your data. Your form will still be the same, with a simple grid binded to the data fetched – Nadeem_MK Sep 24 '13 at 11:04
  • I can output the grid with say 08:00 hours to 18:00hrs, the currently problem is visually displaying the bookings, for example 13:00hrs-14:50hrs – James Smithy Cleave Sep 24 '13 at 11:06
  • Forget winforms. It's useless. You can create this in WPF in 30 minutes. (or spend a thousand dollars to buy DevExpress) – Federico Berasategui Sep 24 '13 at 14:48
  • Just in case you're interested, I did a WPF example of all this while drinking my [mate](http://en.wikipedia.org/wiki/Mate_(beverage)) before lunch (in less than half an hour). Let me know and I'll post it as an answer. You could even integrate that in your existing winforms application by using an `ElementHost`. – Federico Berasategui Sep 24 '13 at 15:56
  • Highcore if you have something like that it would be brilliant, been looking at wpf application but looked a little scary to try on a big project, will learn though! – James Smithy Cleave Sep 27 '13 at 10:07
  • 1
    @HighCore Saw some of your posts on converting winforms to WPF and now its not that easy, I have been programming vb.net for 6 years and c# for 2 and understand php/html so guessing WPF might be easy enough to pick up, depending on that example you talk about above I may recode my project in WPF – James Smithy Cleave Sep 27 '13 at 10:38

3 Answers3

4

Posting this answer because the OP requested it:

This is how you do that in WPF:

enter image description here

<Window x:Class="MiscSamples.TimeBookings"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MiscSamples"
    Title="TimeBookings" Height="300" Width="300">
<Window.Resources>
    <local:TimeRangeToVerticalMarginConverter x:Key="VerticalMarginConverter"/>
    <local:TimeRangeHeightConverter x:Key="HeightConverter"/>
</Window.Resources>

<ScrollViewer>
    <Grid>
        <ItemsControl ItemsSource="{Binding Available}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="Black" BorderThickness="1" Height="60">
                        <TextBlock Text="{Binding StringFormat='hh tt'}"
                               HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </Border>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>

        <ItemsControl ItemsSource="{Binding Bookings}">
            <ItemsControl.ItemContainerStyle>
                <Style TargetType="ContentPresenter">
                    <Setter Property="Margin" Value="{Binding Converter={StaticResource VerticalMarginConverter}}"/>
                    <Setter Property="Height" Value="{Binding Converter={StaticResource HeightConverter}}"/>
                    <Setter Property="VerticalAlignment" Value="Top"/>
                </Style>
            </ItemsControl.ItemContainerStyle>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Border Background="#601050FF" BorderBrush="LightSkyBlue" BorderThickness="1"
                        x:Name="Border">
                        <Viewbox Stretch="Uniform">
                            <TextBlock Text="Booked" FontWeight="Bold" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="16">
                            <TextBlock.LayoutTransform>
                                <RotateTransform Angle="-45"/>
                            </TextBlock.LayoutTransform>
                            </TextBlock>
                        </Viewbox>
                        <Border.ToolTip>
                            <ToolTip>
                                <StackPanel>
                                    <TextBlock>
                                        <Run Text="From" FontWeight="Bold"/>
                                        <Run Text="{Binding StartString, Mode=OneWay}"/>
                                    </TextBlock>

                                    <TextBlock>
                                        <Run Text="To" FontWeight="Bold"/>
                                        <Run Text="{Binding EndString,Mode=OneWay}"/>
                                    </TextBlock>
                                </StackPanel>
                            </ToolTip>
                        </Border.ToolTip>

                    </Border>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Grid/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </Grid>
</ScrollViewer>

Code Behind:

public partial class TimeBookings : Window
{
    public TimeBookings()
    {
        InitializeComponent();

        DataContext = new TimeBookingsViewModel();
    }
}

ViewModel:

public class TimeBookingsViewModel
{
    public ObservableCollection<DateTime> Available { get; set; } 

    public ObservableCollection<TimeRange> Bookings { get; set; }

    public TimeBookingsViewModel()
    {
        Available = new ObservableCollection<DateTime>(Enumerable.Range(8, 11).Select(x => new DateTime(2013, 1, 1).AddHours(x))); 

        Bookings = new ObservableCollection<TimeRange>(); 

        Bookings.Add(new TimeRange(8, 0, 9, 50) {Base = TimeSpan.FromHours(8)});
        Bookings.Add(new TimeRange(10, 0, 11, 00) { Base = TimeSpan.FromHours(8) });
        Bookings.Add(new TimeRange(12, 00, 13, 30) { Base = TimeSpan.FromHours(8) });
    }
}

Data Item:

public class TimeRange
{
    public TimeSpan Base { get; set; }

    public TimeSpan Start { get; set; }

    public TimeSpan End { get; set; }

    public string StartString { get { return new DateTime(Start.Ticks).ToString("hh:mm tt"); } }

    public string EndString { get { return new DateTime(End.Ticks).ToString("hh:mm tt"); } }

    public TimeRange(int starthour, int startminute, int endhour, int endminute)
    {
        Start = new TimeSpan(0, starthour, startminute, 0);
        End = new TimeSpan(0, endhour, endminute, 0);
    }
}

And a few helpers (Converters and such):

public class TimeRangeToVerticalMarginConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!(value is TimeRange))
            return null;

        var range = (TimeRange) value;

        return new Thickness(2, range.Start.TotalMinutes - range.Base.TotalMinutes, 2, 0);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public class TimeRangeHeightConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!(value is TimeRange))
            return null;

        var range = value as TimeRange;

        return range.End.Subtract(range.Start).TotalMinutes;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
  • The UI is separate from Data and Logic by using MVVM, DataBinding and The WPF Mentality
  • This keeps your code behind almost empty and your application code really clean, by just dealing with your own classes and properties, and leaving the UI alone.
  • No "owner draw", no P/Invoke (whatever that means), no complicated size/position calculations, and no crappy procedural "drawing code". Only beautiful declarative XAML and DataBinding to simple, simple properties.
  • The UI is created by using 2 ItemsControls with different DataTemplates (one for the "background" hour boxes, and the other for the bookings visual representation)
  • The "Booked" textblock is inside a Viewbox which makes it stretch to the available size. You can change that if you want, but I could not imagine a better way to make the text fit the available space for different bookings.
  • I even took the time to add the nice descriptive ToolTip. You can really do what you want in WPF.
  • I strongly suggest you read all the linked material in this post, mostly Rachel's "WPF Mentality" and related blog posts. Let me know if you need further help.

Bottom Line:

Forget winforms, it's too limited, it doesn't have (real) databinding, it requires a lot of code to do less, it does not support any level of customization, and it forces you to create shitty Windows 95 like UIs.

WPF Rocks: Just copy and paste my code in a File -> New Project -> WPF Application and see the results for yourself.

Community
  • 1
  • 1
Federico Berasategui
  • 43,562
  • 11
  • 100
  • 154
  • So I picked up WPF a few months back and thought, this looks too complicated and I am too busy to learn, I'm surprised just how easy it is and your code rocks! Thanks man, life saver :) – James Smithy Cleave Sep 27 '13 at 12:45
  • quick question, Just tried to start the datetime from 8am-6pm, so change the Range to (8,11) which gave start 8, finish 6pm, but in the view the times still say from 1am to 3:50 am when i hover over it, is there a way to change this? Available = new ObservableCollection(Enumerable.Range(8, 11).Select(x => new DateTime(2013, 1, 1).AddHours(x))); Bookings = new ObservableCollection(); – James Smithy Cleave Sep 27 '13 at 14:31
  • @JamesSmithyCleave the data is coming from the `TimeRange`s, their constructor expects `int starthour, int startminute, int endhour, int endminute`, change these values when creating them. – Federico Berasategui Sep 27 '13 at 14:36
  • This is the change I made, but still getting problems: Img: http://i42.tinypic.com/sg06ro.png It looks like the bookings needs a range too but cannot see how `code public TimeBookingsViewModel() { Available = new ObservableCollection(Enumerable.Range(8, 11).Select(x => new DateTime(2013, 1, 1).AddHours(x))); Bookings = new ObservableCollection(); Bookings.Add(new TimeRange(8, 0, 9, 50)); Bookings.Add(new TimeRange(10, 0, 11, 00)); Bookings.Add(new TimeRange(12, 00, 13, 30)); }` – James Smithy Cleave Sep 27 '13 at 14:46
  • @JamesSmithyCleave I have modified the sample so that it supports non-zero (00:00) base starting time. Check the new code. – Federico Berasategui Sep 27 '13 at 15:08
  • Thanks worked perfectly! Will look over both codes to see what changed, thanks again – James Smithy Cleave Sep 27 '13 at 15:19
  • 1
    @jamesSmithyCleave I basically changed the margin converter to now accept the whole TimeRange entity and perform some calculations to determine what's the `Margin` supposed to be based on `TimeRange.Base` starting time and `TimeRange.Start` value. – Federico Berasategui Sep 27 '13 at 15:22
  • need some advice on how I can reference this control from an element container, ran out of time to convert to WPF so using element container but cannot seam to add bookings remotely, added a class public void AddBooking but still no luck! Cheers – James Smithy Cleave Oct 09 '13 at 02:18
  • Ignore comment, fogured it out lol, Tired.... Basically create a new instance of TimeBookingsViewModel, add booking then set DataContext to the new instance of the ViewModel... No need for public methods etc! Cheers – James Smithy Cleave Oct 09 '13 at 02:20
1

Evaluate the time you will spend to develop your control, multiply this by your cost/hour, add some bug you will (for sure) produce, and compare this to some existant, well tested solutions:

http://www.telerik.com/products/winforms/scheduler.aspx

http://www.devexpress.com/Products/NET/Controls/WinForms/Scheduler/

I suggest you to buy your control (or some).

Persi
  • 335
  • 1
  • 11
1

Your best bet is to create a custom control inheriting from a similar control, for your example something along the lines of a picture-box may be beneficial. For a (slightly outdated C++) walkthough of custom controls see: http://msdn.microsoft.com/en-us/library/ms364048(v=vs.80).aspx

As far as custom-drawing setup goes: http://msdn.microsoft.com/en-us/library/windows/desktop/bb761817(v=vs.85).aspx

As a generalization, the idea is as follows: Capture the WM_PAINT event, and in that event render a pre-drawn image to the control (this is most often done by creating a paint surface, then copying that into the render-able pain-area of the control) this method avoids any 'flicker'.

The drawing commands are mostly simple, 'drawline(xy_start, xy_end).

Finally, for handling the time of day, if you take the (rendersurface.height / (24*60)) you will have a converstion from time to pixels. eg:

double convert_Size = (rendersurface.height / (24*60)); //height / Hours_in_day * Minites
int time = (hour * 60) + minite_past_hour;
Pixels_from_top = time * convert_Size;

Pixels_from_top is now the pixel-y coordinate of that time during the day.

John Bargman
  • 1,267
  • 9
  • 19