I have to create a schedule control using WPF XAML for displaying items with duration as little as 1 seconds and potentially as large as couple of hours.
First thing which seems clear is that I will need some kind of zoom capability, for drilling into the items with very short durations and zooming out to view the items with very large durations.
I have a UI style in mind where by when the control is zoomed to view the large items, any small duration items which generally occur between the large duration items are some how collected in to a 'bucket' which has a height >= a minimum display height. So that I can perhaps hover a mouse over this bucket and get a tool tip which renders the contained items into a time ordered list view.
I am at a loss as to where to begin. I feel that I could perhaps achieve something in the Measure/Arrange overrides of a custom Panel. But I think this could adversely affect render performance. If I were to do it this way, I guess I would need to inject a 'bucket' view into the panels children collection, but that would break the use of ItemTemplate in my Custom Items Control?
My second though is that I create a custom observable collection which has a bindable Scale property which then organises the collection into 'buckets' of a duration large enough to be displayable.
Does anyone have any thoughts on how I should best approach this problem?
I made a similar sounding control which was actually used for inputting employee time shifts. It uses a simple data type in an ObservableCollection data bound to a ListBox.ItemsSource property. I defined a DataTemplate to design the look of each time segment in the UI. It looks like this:
There are certain ComboBoxes along the top which, when changed, change the number of items in the collection. So for example, when the Minutes/segment ComboBox value is changed to Thirty, twice the number of (same sized) items appear in the UI, each now relating to thirty minutes, not sixty. This is done by simply calling a method that repopulates the collection from the relevant property setters. Here is an example:
public TimeSegmentDivision MinutesPerSegment
{
get { return minutesPerSegment; }
set
{
minutesPerSegment = value;
InitializeTimeSegmentsRowViewModels();
NotifyPropertyChanged("MinutesPerSegment");
}
}
private void InitializeTimeSegmentsRowViewModels()
{
if (DayCount == 5) AdjustStartDate(); // (DayCount == 5 is 'Monday - Friday')
DateTime tempDate = Date;
AllTimeSegments = new TimeSegmentsCollection();
for (int m = 0; m < DayCount; m++)
{
TimeSegmentsRowViewModel viewModel = new TimeSegmentsRowViewModel();
viewModel.TimeSegments = InitializeTimeSegments();
AllTimeSegments.Add(viewModel);
date = Date.AddDays(1);
}
NotifyPropertyChanged("AllTimeSegments");
date = tempDate;
}
The specifics of the InitializeTimeSegmentsRowViewModels method is unimportant here, but you should be able to get the right idea from this example.
I worked on custom schedule for Silverlight. It has similar layout loading process so I would try to answer.
I guess your main problem lies in the 'time bar'... Stop thinking about schedule as the big consistent control, it's some pack of custom controls. Take a look on grid. Such controls has a lot peaces.
At start we have to solve first problem - time bar. Right implementation will lead you to painless future))) So time bar it's some control that contains total time, or duration of the employee labors(this value can be set by user). And such thing like step , exactly step and duration will tell you size and position of labors. For time bar control step will tell where are visual ticks should be displayed. I mean time bar will look like rulers, but with time values instead of inches. So we are getting next problem - how to translate time into pixels? Unfortunately I didn't found best solution - I assumed 1min is equal to 1.6 pixels It was perfect for me... But you will have dynamic value that will dynamically increase or decrease pixel length of step. So we have total duration in time, we can convert it into pixels length. But total length could be much bigger then available size. So, now we have another time bar property - visual duration...
okay, I guess you understand my way of thinking... Changing pixel size of step you will get perfect zoom(not step time size, but pixel length).
And yes, you are on the right way if you want create custom panels with overrated measure\arrange methods. And don't worry about item template. It's easiest part. Your parent schedule control will have items source property of object type. All your labors will be content controls....
Related
I am developing a WPF application that displays complex graphics to a window. I need these graphics to be highly dynamic, meaning that for instance lines can appear, disappear or just be moved at any time (by mouse interaction or programmatically).
Is there a way to achieve that without rebuilding the complete display list every time ? A typical case is that of the plot of a function where the data points can move, but not the rest. The points may be computed on the fly and not remembered in an array, so referring to their graphical representation is tricky.
Code example (on request):
// Add a Line Element
myLine = new Line();
myLine.Stroke = System.Windows.Media.Brushes.LightSteelBlue;
myLine.X1 = 1;
myLine.X2 = 50;
myLine.Y1 = 1;
myLine.Y2 = 50;
myLine.HorizontalAlignment = HorizontalAlignment.Left;
myLine.VerticalAlignment = VerticalAlignment.Center;
myLine.StrokeThickness = 2;
myGrid.Children.Add(myLine);
Update:
Assume I plot three functions. I draw the axis, the legends, the tick marks... and three curves.
Now is there a better way than redrawing everything if I want to remove the second curve ?
Is there a way to achieve that without rebuilding the complete display list every time ?
That's exactly what the MVVM pattern in general, and WPF templates together aim to provide. Specifically, you don't describe much of what you're trying to achieve except for this:
lines can appear, disappear or just be moved at any time
To me that sounds like two model classes, one for points (X and Y coordinates) and one for lines (that hold references to two points). The point model is observable (ie it implements INotifyPropertyChanged correctly and fully), while the line model should never change.
Coming to your view model, what you have is a collection of entities (something like IEntity), from which both models are derived. So simply a read only property of type ObservableCollection<IEntity>. When you want to move a point from a line, simply update its X and/or Y coordinate. Want to add a line? Add the points and the line connecting them to your collection. Same for deletion. Easy!
And the last part, your view. This holds an ItemsControl bound to your view model collection, with the panel template set to <Canvas/> (to be able to set location directly) and <DataTemplate> resources, one for each type of model you have:
Lines are simple, they're simply drawn in place by binding their four location properties to the X and Y coordinates of your two point models in your line model.
Points are where you get fancy, their purpose is to provide manipulators to move them around, ie a user control that responds to drag events for example and that updates its bound properties. Because lines hold references to your points, simply updating the point directly will reposition both the affected point visually, as well as the line that connects it.
I have a ListBox with a horizontal WrapPanel inside to create a grid of content.
It produces the correct layout but it's really slow with greater than a hundred or so items.
I see a few half-baked attempts at virtualising a WrapPanel on google but none of them seem production ready.
Am I missing a trick?
How do I get performant and flexible (needs to reflow on resize) panel layouts?
(Note: cells are fixed size).
Yes, virtualizing is the correct step. WPF cant handle large amounts of UI items at once.
Writing your own VirtualizingWrapPanel takes quite a lot of experience in WPF. WPF has no "ready" solution, so you either have to write it, or use someone else work.
Unlike VirtualizingStackPanel which offers the same functionality of StackPanel, this does not fully behave as the default WrapPanel that comes with WPF, there are some reasons for this:
1- WrapPanels can grow in two directions, while StackPanels only grow
vertically or horizontally. Also, the growth in the direction
orthogonal to the panel's orientation depends on the size of the items
in the direction of the panel's orientation, so it is impossible to
wrap into a new line/column unless the size of all items on the last
line/column is known, but this is impossible if the two directions of
the panel are being virtualized(since it does non-pixel based
measurement for performance improvement).
2- The size of the extent is the same as the number of
lines/columns(sections). On the StackPanel this is not a problem:
Since only one item per section is allowed, the extent width/height is
the number of items. On the WrapPanel, the number of sections depends
highly on the size of items, and since the size of the items is not
fixed and can change in any direction, it is impossible to calculate
the number of sections on a WrapPanel.
The following solution was adopted when developing this panel:
1- The panel's extent is never known for sure. Instead, the number of
sections is estimated after each MeasureOverride using the following
expression : 'numberofitems/averageitemspersection'. Since the average
number of items per section is updated after each scrolling, the
extent of the panel is dynamic. The extent will be static if the items
size in the panels orientation is fixed.
2- The items section/sectionindex can only be calculated sequentially,
so if you are viewing the first items and you jump to a section that
bypasses one or more unrealized sections, the panel will use the
estimation to know which item is the first visible, this will be
corrected if you go back to a realized section and go back
sequentially. For example: if the panel knows that item 12 is in
section 1 and the panel estimates 10 items per section,(section 0 is
the first), if you jump to section 9 the panel will show the 100th
item as the first visible item(that will only be correct if from
section 1 to 9 there are exactly 10 items per section). But if you go
back to section 1 and access all the sections sequentially until you
reach section 9, then the panel will store all the items sections
correctly so no further estimations will be made up to section 10.
3- Normally a WrapPanel inside a scrollviewer would be allowed to
scroll vertically and horizontally, but since I can only virtualize
one direction, this wrappanel will only scroll in the direction
orthogonal to the panels orientation.(Meaning you shouldnt set the
Virtualizing Panel Height/Width explicitly).
Copied from; http://virtualwrappanel.codeplex.com/
I didn't understand what you meant by fixed cells, but if you have fixed width / height per item, you could write better virtualization than the one I provided(VirtualWrapPanel). Just browse through code and try to understand what is going on.
What you call "doesnt quite work" is probably because it's not possible to make nice VirtualizingWrapPanel that would work exactly like the one WrapPanel(due to fact that the specific row of items depends on the next items and so on. ), but if you combine it with the fact that each item has the same width/height, I believe you could do better.
Also, you can get started by writing your own solution, based on these articles;
One: http://blogs.msdn.com/dancre/archive/2006/02/06/implementing-a-virtualized-panel-in-wpf-avalon.aspx
Two: http://blogs.msdn.com/dancre/archive/2006/02/13/531550.aspx
Three: http://blogs.msdn.com/dancre/archive/2006/02/14/532333.aspx
Four: http://blogs.msdn.com/dancre/archive/2006/02/16/implementing-a-virtualizingpanel-part-4-the-goods.aspx
How about try to use Grid and add the item to Grid such as :
( But I didn't test its performance)
public void InitializeGridAsFourColumns()
{
for (int i = 0; i < 4; i++)
{
_customGrid.ColumnDefinitions.Add(new ColumnDefinition());
}
}
private static int row = -1;
private static int column = 0;
public void AddItem(item)
{
//Add new RowDefinition every 4 items
if (column % 4 == 0)
{
RowDefinition rd = new RowDefinition();
rd.Height = GridLength.Auto;
_customGrid.RowDefinitions.Add(rd);
row++;
}
item.SetValue(Grid.RowProperty, row);
item.SetValue(Grid.ColumnProperty, column % 4);
column++;
_customGrid.Children.Add(item);
}
Being new to silverlight I'm struggling to 'get going' with the following.
Basically I wish to create some form of grid like control (custom or user?).
The idea is similar to that of a planner. Along the top are times (set intervals). Downwards are subjects. Then over the grid like background rectangles (or something) indicate when the subject is planned for.
The actual design of the above is not the issue. i.e. a grid with ractangles overlaid. But my issue is I wish this grid to be scrolled up and down (with bounds fixing the top and bottom when the subject lines start and end). And also the grid to be scrolled left and right (with bounds fixing how far left and right it can scroll, current time & 3 days into future).
Based on the above needs, I don't wish to create a control which is very large, and just dragged into view (unless this is the only way?) but instead show the grid at a current time and when dragged dynamically load the next few hours worth of content, possibly with a few hours buffer.
The appearance I am seeking is it looking like it is one massive control, but truely its not, its dynamic.
Does this make sense? Am I worrying about nothing? Should I create a massive grid well into the future and then just handle the load of data dynamically over the top? Its just my concern if I want a grid 3 month into the future this would be massive and a waste of memory.
I'm struggling to find examples on the net, but feel this maybe to do with me not knowing what to search for. This isn't about getting a detailed answer and someone doing it for me, but instead about guidance pointing me in the right direction.
Many thanks
About the up-down scroll: you can simply put a grid containing your data in a ScrollViewer control - this will handle all the scrolling for you. Another solution would be using a listbox control - this is better if you use MVVM. You can bind it to a data source and set as data template a custom control.
For the left-right scroll. I'm thinking you could use gestures for this. Like - catch left-to-right and right-to-left flicks and change the data in your grid / listbox according to the gesture's direction. You could also place two buttons at the top of the grid to handle scrolling from one day to the other (just like in the calendar controls: gestures + buttons).
I have built a UserControl for display a list of other UserControls which themselves are bound to individual data objects. The link below shows an example implementation of this control.
Each of the individual user rows is its own UserControl/XtraUserControl, laid out in a FlowLayoutPanel.
The problem I have is with perfomance, to populate the list above takes around 500ms (excluding any data loading) - this is a combination of creating each control and then adding them to the FlowLayoutPanel using the AddRange(controls[]) method.
Does anyone know any way I can improve perfomance here? Do I have to manually paint the items instead of using User Controls?
Thanks in advance.
EDIT: I've added my own response below showing the solution I have stuck with for now.
Whether manually painting would help is a guess. Even if it were right (which I doubt) it's better not to guess.
I've seen this kind of issue before, and chances are there's a lot of stuff that goes on in the binding.
The way I've solved the problem is with this approach, but it's definitely "out there" in terms of programmer acceptance.
I gues you are using devexpress controls because you mention XtraUserControl. If so, why don't use an XtraGrid?You can add images column and button columns, and I think you'll get better performance and simpler/less code
First of all, try use pair SuspendLayout()/ResumeLayout(), then it has sense to stop painting at all by hiding the container control until all child usercontrols added.
Anyway, placing lots of child controls to a container is not a good idea.
You can have the same result by using highly customized grid or by custom painting (which is preferable).
Good luck!
I had a brainwave for another solution which I'm not quite sure is appropriate. I would really appreciate any feedback on this.
Two rationales led to this solution:
Firstly I wanted the flexibility of creating rows like any other control.
Secondly the lists that would use this approach only intend to display brief chunks of data, never more than say 20 items - for anything larger, ListViews are used.
So anyway, what I decided to do was cache a set number of the Panels (I've referred to the custom controls or rows as Panels throughout the code) and to build up this cache as the control is created. When populating the control with BusinessObjects, the existing cached Panels are displayed with their bound BusinessObject. You can see how this works exactly from the code below, so there is no need for a in-depth description.
The fact of the matter is that I've managed to reduce the data population time (after the initial cache setup of around 180ms for 10 Panels) from 500ms to 150ms for the list shown in the image above.
private int cacheSize = 10;
private List<P> cachedPanels = new List<P>(10);
private void InitItems()
{
this.contentPanel.SuspendLayout();
// Create the cached panels from the default cache value.
for (int i = 0; i < cacheSize; i++)
cachedPanels.Add(new P() { Margin = new Padding(0), Visible = false });
this.contentPanel.Controls.AddRange(cachedPanels.ToArray());
this.contentPanel.ResumeLayout(true);
}
private void PopulateListFromCache()
{
this.contentPanel.SuspendLayout();
// Iterate against both BusinessObjects and Panels to ensure that nothing is missed, for
// instance, where there are too many panels, the rest are hidden, and too many Business
// Objects, then more Panels are created.
for (int i = 0; i < this.businessObjects.Count || i < this.cachedPanels.Count; i++)
{
if (i >= this.cachedPanels.Count)
{
// Here, we have more BusinessObjects than Panels, thus we must create
// and assign a new panel.
this.cachedPanels.Add(new P() { Margin = new Padding(0) });
this.cachedPanels[i].Item = this.businessObjects[i];
this.contentPanel.Controls.Add(this.cachedPanels[i]);
}
else if (i >= this.businessObjects.Count)
{
// Here, we still have Panels cached but have run out of BusinessObjects,
// let's just hide them and clear their bindings.
this.cachedPanels[i].Item = default(T);
this.cachedPanels[i].Visible = false;
}
else
{
// Here, we have both BusinessObjects and Panels to put them in, so just
// update the binding and ensure the Panel is visible.
this.cachedPanels[i].Item = this.businessObjects[i];
this.cachedPanels[i].Visible = true;
}
}
this.contentPanel.ResumeLayout(true);
}
Obviously, more optimizations can be made, such as un-caching Panels after a certain amount of time of not being used etc. Also, I'm not entirely sure if keeping these controls - which are rather simple - in a cache will affect memory usage much.
If anyone can think of any other pointers then please, be my guest. Oh, and if you got this far, then thank you for reading this.
A client has asked for a display to flick over like an airport display screen, ie each row flicks over when information changes.
I am not sure which is the best control to use, or the method of getting each row to transform one after the other.
any suggestions woul b gratfully accepted
John
Here's what I would do in general concept..
Make a regular panel of, say 50px high. (This is arbitrary but this panel just holds the size in place so the control doesn't shrink with its contents.)
Create a panel inside that one that will be the 'animated' panel.
When it's time for information to animate, create a storyboard that uses a transformation to "stretch" the height down to 0, change the content to the updated information, then tranform stretch the height back to 50px. This will create the illusion that the panel is flipping over.
If you make this a user control, then you could simply add however many "rows" you needed of this control to a StackPanel to make your screen.
The best way of representing this effect easily is to randomize the text during the change.
Patrick Long implemented this effect as a custom animation here