I need to generate a printed form using XAML that has a header grid and a variable number of rows that can result in multiple pages as the number of rows increases. The header must appear on each page and each row may vary in height due to text wrapping within the row. I am currently trying to use ActualHeight of the ItemsControl (rows container) to determine when to generate a new page, but ActualHeight always has a value of zero.
My "XAML_Form" has the following structure. A grid is used in the ItemTemplate to allow aligning columns in the rows with columns in the header grid.
<Grid Width="980" Height="757">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Name="_headerControl" Grid.Row="0"/>
<ItemsControl Name=_rowsControl ItemsSource={Binding Rows} Grid.Row="1">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Grid>
Our architecture has a report class that handles adding footers, page numbers, and aggregating the pages into a PDF. This report class has a nested ViewModel for each XAML View page. The ViewModel for each page uses a backing List of row objects:
List<RowClass> RowsList;
The ViewModel also has an ICollectionView that is used to bind as the ItemsSource:
ICollectionView Rows = new ListCollectionView(RowsList);
My report class has a CreatePages method which contains code like this:
IList<XAML_Form> pages = new List<XAML_Form>();
var vm = new PageViewModelClass();
var page = new XAML_Form { DataContext = vm };
page.InitializeComponent();
page.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity);
page.Arrange(new Rect(new Point(0,0), page.DesiredSize));
var maxRowsHeight = page.DesiredSize.Height - page._headerControl.ActualHeight;
pages.Add(page);
var rowsOnPage = 0;
foreach (var row in sourceRowsObjectList)
{
rowsOnPage++;
vm.RowsList.Add(row);
vm.Rows.Refresh();
page.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity);
page.Arrange(new Rect(new Point(0,0), page.DesiredSize));
if (page._rowsControl.ActualHeight <= maxRowsHeight)
continue;
// The rows exceed the available space; the row needs to go on the next page.
vm.RowsList.RemoveAt(--rowsOnPage);
vm = new PageViewModelClass();
vm.RowsList.Add(row);
rowsOnPage = 1;
page = new XAML_Form { DataContext = vm };
page.InitializeComponent();
pages.Add(page);
}
return pages;
The initial Measure/Arrange does provide me with the expected value for maxRowsHeight. And the generated form looks fine for a single page with a few rows. My specific problem is: Why is page._rowsControl.ActualHeight always zero? And generally, is there a better approach to this problem?
Here is a solution. We are trying to separate the view concerns from the view model concerns, so there are still improvements to be made.
The CreatePages method in the report class is now:
private static IEnumerable<XAML_Form> CreatePages()
{
IList<XAML_Form> pages = new List<XAML_Form>();
int rowCount = sourceRowsObjectList.Count;
int remainingRowCount = rowCount;
do
{
var pageVm = new PageViewModelClass();
var page = new XAML_Form(pageVm);
pages.Add(page);
int numberOfRowsToAdd = Math.Min(remainingRowCount, XAML_Form.MaxNumberOfRows);
pageVm.AddRows(sourceRowsObjectList.Skip(rowCount - remainingRowCount).Take(numberOfRowsToAdd));
remainingRowCount -= numberOfRowsToAdd;
while (page.AreRowsOverflowing())
{
pageVm.RemoveLastRow();
remainingRowCount++;
}
} while (remainingRowCount > 0);
return pages;
}
The pertinent XAML_Form code behind is as follows:
private static int _maxNumberOfRows = -1;
public XAML_Form(PageViewModelClass viewModel)
{
InitializeComponent();
Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
Arrange(new Rect(new Point(0, 0), DesiredSize);
ViewModel = viewModel;
}
public PageViewModelClass ViewModel
{
get { return (PageViewModelClass)DataContext; }
private set { DataContext = value; }
}
public static int MaxNumberOfRows
{
get // Compute this only once, the first time it is called.
{
if (_maxNumberOfRows < 0) return _maxNumberOfRows;
var page = new XAML_Form();
var singleRowCollection = new object[] { null; }
page._rowsControl.ItemsSource = singleItemCollection;
page._rowsControl.UpdateLayout();
var rowHeight = page._rowsControl.ActualHeight;
_maxNumberOfRows = (int)((page.DesiredSize.Height - page._headerControl.ActualHeight) / rowHeight);
page._rowsControl.ItemsSource = null;
return _maxNumberOfRows;
}
}
// Call this method as rarely as possible. UpdateLayout is EXPENSIVE!
public bool AreRowsOverflowing()
{
_rowsControl.UpdateLayout();
return _rowsControl.ActualHeight > DesiredSize.Height - _headerControl.ActualHeight;
}
Related
I copied the code from the Xaml Controls Gallery. When I run and drag the listitem, the app closes. If I run in debug, F10 doesn't lead me to the part of the code that's breaking. In fact, the app stays open but nothing happens. I'm stumped.
(this is from winui3-preview 4)
Contact.txt
https://github.com/microsoft/Xaml-Controls-Gallery/blob/master/XamlControlsGallery/Assets/Contacts.txt
// ListBoxPage.xaml (I know bad naming)
<Page
x:Class="NavTest.ListBoxPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:NavTest"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local1="using:Windows.ApplicationModel.Contacts"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.Resources>
<!--
ListViews with grouped items must be bound to a CollectionViewSource, as shown below.
This CollectionViewSource is defined in the XAML below, but is bound to an ItemsSource in the C#
code-behind. See the C# code below for more details on how to create/bind to a grouped list.
-->
<CollectionViewSource x:Name="ContactsCVS" IsSourceGrouped="True"/>
<!--
In this example, the ListView's ItemTemplate property is bound to a data template (shown below)
called ContactListViewTemplate, defined in a Page.Resources section.
-->
<DataTemplate x:Key="ContactListViewTemplate" x:DataType="local:Contact">
<TextBlock Text="{x:Bind Name}" x:Phase="1" Margin="0,5,0,5"/>
</DataTemplate>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ListView
Grid.Row="0"
Grid.Column="0"
x:Name="BaseExample"
SelectionMode="Extended"
ItemsSource="{x:Bind ContactsCVS.View, Mode=OneWay}"
ItemTemplate="{StaticResource ContactListViewTemplate}"
BorderThickness="1"
BorderBrush="{ThemeResource SystemControlForegroundBaseMediumLowBrush}"
Width="550"
Height="400"
CanDragItems="True"
CanReorderItems="True"
AllowDrop="True"
DragItemsStarting="BaseExample_DragItemsStarting"
DragOver="BaseExample_DragOver"
Drop="BaseExample_Drop"
HorizontalAlignment="Left">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel AreStickyGroupHeadersEnabled="True"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate x:DataType="local:GroupInfoList">
<Border>
<TextBlock Text="{x:Bind Key}" Style="{ThemeResource TitleTextBlockStyle}" />
</Border>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
<ListView
Grid.Row="0"
Grid.Column="1"
x:Name="BaseExample2"
SelectionMode="Extended"
ItemTemplate="{StaticResource ContactListViewTemplate}"
BorderThickness="1"
BorderBrush="{ThemeResource SystemControlForegroundBaseMediumLowBrush}"
Width="550"
Height="400"
CanDragItems="True"
CanReorderItems="True"
AllowDrop="True"
DragItemsStarting="BaseExample2_DragItemsStarting"
DragOver="BaseExample2_DragOver"
DragEnter="BaseExample2_DragEnter"
Drop="BaseExample_Drop"
HorizontalAlignment="Left">
</ListView>
</Grid>
// ListBoxPage.xaml.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.Storage;
using Windows.UI.WebUI;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace NavTest
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class ListBoxPage : Page
{
ObservableCollection<Contact> contacts1 = new ObservableCollection<Contact>();
ObservableCollection<Contact> contacts2 = new ObservableCollection<Contact>();
public ListBoxPage()
{
this.InitializeComponent();
}
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
// The ItemsSource for the ListView is generated by a method of the Contact class called
// GetContactsAsync().This method pulls data from an internal data source and creates
// Contact objects from that data. Those Contact objects are placed in a collection
// which is returned from the GetContactsAsync() function.
contacts1 = await Contact.GetContactsAsync();
contacts2 = new ObservableCollection<Contact>();
BaseExample.ItemsSource = contacts1;
BaseExample2.ItemsSource = contacts2;
ContactsCVS.Source = await Contact.GetContactsGroupedAsync();
}
private void BaseExample_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
// Prepare a string with one dragged item per line
StringBuilder items = new StringBuilder();
foreach (Contact item in e.Items)
{
if (items.Length > 0) { items.AppendLine(); }
if (item.ToString() != null)
{
// Append name from contact object onto data string
items.Append(item.ToString() + " " + item.Company);
}
}
// Set the content of the DataPackage
e.Data.SetText(items.ToString());
e.Data.RequestedOperation = DataPackageOperation.Move;
}
private void BaseExample_DragOver(object sender, DragEventArgs e)
{
e.AcceptedOperation = DataPackageOperation.Move;
}
private async void BaseExample_Drop(object sender, DragEventArgs e)
{
ListView target = (ListView)sender;
if (e.DataView.Contains(StandardDataFormats.Text))
{
DragOperationDeferral def = e.GetDeferral();
string s = await e.DataView.GetTextAsync();
string[] items = s.Split('\n');
foreach (string item in items)
{
// Create Contact object from string, add to existing target ListView
string[] info = item.Split(" ", 3);
Contact temp = new Contact(info[0], info[1], info[2]);
// Find the insertion index:
Windows.Foundation.Point pos = e.GetPosition(target.ItemsPanelRoot);
// Find which ListView is the target, find height of first item
ListViewItem sampleItem;
if (target.Name == "BaseExample")
{
sampleItem = (ListViewItem)BaseExample2.ContainerFromIndex(0);
}
// Only other case is target = DragDropListView2
else
{
sampleItem = (ListViewItem)BaseExample.ContainerFromIndex(0);
}
// Adjust ItemHeight for margins
double itemHeight = sampleItem.ActualHeight + sampleItem.Margin.Top + sampleItem.Margin.Bottom;
// Find index based on dividing number of items by height of each item
int index = Math.Min(target.Items.Count - 1, (int)(pos.Y / itemHeight));
// Find the item that we want to drop
ListViewItem targetItem = (ListViewItem)target.ContainerFromIndex(index); ;
// Figure out if to insert above or below
Windows.Foundation.Point positionInItem = e.GetPosition(targetItem);
if (positionInItem.Y > itemHeight / 2)
{
index++;
}
// Don't go out of bounds
index = Math.Min(target.Items.Count, index);
// Find correct source list
if (target.Name == "BaseExample")
{
// Find the ItemsSource for the target ListView and insert
contacts1.Insert(index, temp);
//Go through source list and remove the items that are being moved
foreach (Contact contact in BaseExample2.Items)
{
if (contact.FirstName == temp.FirstName && contact.LastName == temp.LastName && contact.Company == temp.Company)
{
contacts2.Remove(contact);
break;
}
}
}
else if (target.Name == "BaseExample2")
{
contacts2.Insert(index, temp);
foreach (Contact contact in BaseExample.Items)
{
if (contact.FirstName == temp.FirstName && contact.LastName == temp.LastName && contact.Company == temp.Company)
{
contacts1.Remove(contact);
break;
}
}
}
}
e.AcceptedOperation = DataPackageOperation.Move;
def.Complete();
}
}
private void BaseExample_DragEnter(object sender, DragEventArgs e)
{
// We don't want to show the Move icon
e.DragUIOverride.IsGlyphVisible = false;
}
private void BaseExample2_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
if (e.Items.Count == 1)
{
// Prepare ListViewItem to be moved
Contact tmp = (Contact)e.Items[0];
e.Data.SetText(tmp.FirstName + " " + tmp.LastName + " " + tmp.Company);
e.Data.RequestedOperation = DataPackageOperation.Move;
}
}
private void BaseExample2_DragOver(object sender, DragEventArgs e)
{
e.AcceptedOperation = DataPackageOperation.Move;
}
private async void BaseExample2_Drop(object sender, DragEventArgs e)
{
ListView target = (ListView)sender;
if (e.DataView.Contains(StandardDataFormats.Text))
{
DragOperationDeferral def = e.GetDeferral();
string s = await e.DataView.GetTextAsync();
string[] items = s.Split('\n');
foreach (string item in items)
{
// Create Contact object from string, add to existing target ListView
string[] info = item.Split(" ", 3);
Contact temp = new Contact(info[0], info[1], info[2]);
// Find the insertion index:
Windows.Foundation.Point pos = e.GetPosition(target.ItemsPanelRoot);
// Find which ListView is the target, find height of first item
ListViewItem sampleItem;
if (target.Name == "BaseExample")
{
sampleItem = (ListViewItem)BaseExample2.ContainerFromIndex(0);
}
// Only other case is target = DragDropListView2
else
{
sampleItem = (ListViewItem)BaseExample.ContainerFromIndex(0);
}
// Adjust ItemHeight for margins
double itemHeight = sampleItem.ActualHeight + sampleItem.Margin.Top + sampleItem.Margin.Bottom;
// Find index based on dividing number of items by height of each item
int index = Math.Min(target.Items.Count - 1, (int)(pos.Y / itemHeight));
// Find the item that we want to drop
ListViewItem targetItem = (ListViewItem)target.ContainerFromIndex(index); ;
// Figure out if to insert above or below
Windows.Foundation.Point positionInItem = e.GetPosition(targetItem);
if (positionInItem.Y > itemHeight / 2)
{
index++;
}
// Don't go out of bounds
index = Math.Min(target.Items.Count, index);
// Find correct source list
if (target.Name == "BaseExample")
{
// Find the ItemsSource for the target ListView and insert
contacts1.Insert(index, temp);
//Go through source list and remove the items that are being moved
foreach (Contact contact in BaseExample2.Items)
{
if (contact.FirstName == temp.FirstName && contact.LastName == temp.LastName && contact.Company == temp.Company)
{
contacts2.Remove(contact);
break;
}
}
}
else if (target.Name == "BaseExample2")
{
contacts2.Insert(index, temp);
foreach (Contact contact in BaseExample.Items)
{
if (contact.FirstName == temp.FirstName && contact.LastName == temp.LastName && contact.Company == temp.Company)
{
contacts1.Remove(contact);
break;
}
}
}
}
e.AcceptedOperation = DataPackageOperation.Move;
def.Complete();
}
}
private void BaseExample2_DragEnter(object sender, DragEventArgs e)
{
// We don't want to show the Move icon
e.DragUIOverride.IsGlyphVisible = false;
}
}
// C# code-behind
// The data template is defined to display a Contact object (class definition shown below), and the text
// displayed is bound to the Contact object's Name attribute.
public class Contact
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
public string Company { get; private set; }
public string Name => FirstName + " " + LastName;
public static string ContactsPath => $#"{AppDomain.CurrentDomain.BaseDirectory}\Assets\Contacts.txt";
public Contact(string firstName, string lastName, string company)
{
FirstName = firstName;
LastName = lastName;
Company = company;
}
public override string ToString()
{
return Name;
}
#region Public Methods
public static async Task<ObservableCollection<Contact>> GetContactsAsync()
{
string contactsPath = $#"{AppDomain.CurrentDomain.BaseDirectory}\Assets\Contacts.txt";
Uri contactsUri = new Uri(contactsPath, UriKind.RelativeOrAbsolute);
Uri _contactsUri = new Uri(ContactsPath, UriKind.RelativeOrAbsolute);
StorageFile file = await StorageFile.GetFileFromPathAsync(contactsPath);
IList<string> lines = await FileIO.ReadLinesAsync(file);
ObservableCollection<Contact> contacts = new ObservableCollection<Contact>();
for (int i = 0; i < lines.Count; i += 3)
{
contacts.Add(new Contact(lines[i], lines[i + 1], lines[i + 2]));
}
return contacts;
}
// To create a collection of grouped items, create a query that groups
// an existing list, or returns a grouped collection from a database.
// The following method is used to create the ItemsSource for our CollectionViewSource:
public static async Task<ObservableCollection<GroupInfoList>> GetContactsGroupedAsync()
{
// Grab Contact objects from pre-existing list (list is returned from function GetContactsAsync())
var query = from item in await GetContactsAsync()
// Group the items returned from the query, sort and select the ones you want to keep
group item by item.LastName.Substring(0, 1).ToUpper() into g
orderby g.Key
// GroupInfoList is a simple custom class that has an IEnumerable type attribute, and
// a key attribute. The IGrouping-typed variable g now holds the Contact objects,
// and these objects will be used to create a new GroupInfoList object.
select new GroupInfoList(g) { Key = g.Key };
return new ObservableCollection<GroupInfoList>(query);
}
#endregion
}
// GroupInfoList class definition:
public class GroupInfoList : List<object>
{
public GroupInfoList(IEnumerable<object> items) : base(items)
{
}
public object Key { get; set; }
}
}
Drag and Drop is not on the list of supported functionality in and preview 0.5
I think they fixed a few serious bugs in 0.5.
Try again when 0.8 is released.
Remember you are one of the earlierst birds when you use WinUI3 now. You don't get only the worm but also the mud.
I think the problem is not in the posted code, I think you changed App.xaml and App.xaml.cs or (not xor) removed the default reference MainWindow in order to make your app. If your app does not start and does not reach any break point in your Page, it's because it is not in the "execution chain" which starts with App.xaml.
App.xaml calls the MainWindow class (View):
<Application x:Class="MyProject.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyProject"
StartupUri="MainWindow.xaml"> <!-- reference here-->
<Application.Resources>
</Application.Resources>
</Application>
And then, from MainWindow (.xaml and .xaml.cs) you should have your Page referenced.
for my project, i am trying to create a signal channel generator which connects to a toolset and pushes signals into it.
the issue i have is that i have been given the project in a form where the code for the textboxes are in the codebehind file, and i would like them to be in the xaml.
i have a variable which controls the number of channels (viewmodels) which can be changed. which is able to create multiple instances of the same viewmodel on the window. this allows the ability to select different targets inside the tool whcih it is communicating with and be able to pump signals to each target.
here is the code currently in the XAML:
<Window x:Class="SigGeneratorMVVM.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SigGeneratorMVVM"
Title="Signal Generator" Height="370" Width="734" >
<StackPanel Name="MyWindow">
<!--<TextBox Height="23" HorizontalAlignment="Left" Margin="91,20,0,0" Name="CurrentValDisplay" VerticalAlignment="Top" Width="120" />-->
</StackPanel>
</Window>
here is the code for the mainwindow.cs
public partial class MainWindow : Window
{
List<ViewModel> gViewModels;
int gNumChannels = 1;
private System.Threading.Timer mViewUpdateTimer;
private TimerCallback mViewTimerCallback;
private UtilityParticipant mParticipant;
public MainWindow()
{
InitializeComponent();
// Connect as UtilityParticipant
ConnectMesh();
gViewModels = new List<ViewModel>();
for (int i = 0; i < gNumChannels; i++)
{
gViewModels.Add(new ViewModel(mParticipant));
TextBlock CurrentValueText = new TextBlock();
CurrentValueText.Text = "Current Value:";
CurrentValueText.Margin = new Thickness(5);
TextBox CurrentValueBox = new TextBox();
CurrentValueBox.Width = 120;
CurrentValueBox.Name = "CurrentValDisplay" + i.ToString();
CurrentValueBox.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
CurrentValueBox.Margin = new Thickness(10);
CurrentValueBox.SetBinding(TextBox.TextProperty, "CurrentValue");
//CurrentValDisplay.Name = "CurrentValDisplay" + i.ToString();
//CurrentValDisplay.SetBinding(TextBox.TextProperty, "CurrentValue");
TextBlock CurrentFrequencyText = new TextBlock();
CurrentFrequencyText.Text = "Frequency:";
CurrentFrequencyText.Margin = new Thickness(5);
TextBox CurrentFrequencyBox = new TextBox();
CurrentFrequencyBox.Width = 120;
CurrentFrequencyBox.Name = "CurrentFrequencyDisplay" + i.ToString();
CurrentFrequencyBox.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
CurrentFrequencyBox.Margin = new Thickness(10);
CurrentFrequencyBox.SetBinding(TextBox.TextProperty, "Frequency");
Slider FrequencySlider = new Slider();
FrequencySlider.Width = 200;
FrequencySlider.Name = "FrequencySet" + i.ToString();
FrequencySlider.Value= 10;
FrequencySlider.Maximum = 10;
FrequencySlider.Minimum = 0.1;
FrequencySlider.SetBinding(Slider.ValueProperty, "Frequency");
//Create a new stackpanel
StackPanel sp = new StackPanel();
sp.Orientation = Orientation.Vertical;
//Set DataContext of the StackPanel
sp.DataContext = gViewModels[i];
//Add controls created above to the StackPanel
sp.Children.Add(CurrentValueText);
sp.Children.Add(CurrentValueBox);
sp.Children.Add(CurrentFrequencyText);
sp.Children.Add(CurrentFrequencyBox);
sp.Children.Add(FrequencySlider);
//Add the StackPanel to the window
MyWindow.Children.Add(sp);
}
mViewTimerCallback = this.UpdateView;
mViewUpdateTimer = new System.Threading.Timer(mViewTimerCallback, null, 100, 20);
}
Update: I already have a ViewModel which has get set methods for each property (CurrentValue and Frequency for now), would it be sufficient to bind the DataTemplate and ItemsControl to that instead of creating a new model class?
private SigGenChannel mSigGenChannel;
//Constructor
public ViewModel(UtilityParticipant aParticipant)
{
mSigGenChannel = new SigGenChannel(aParticipant);
}
public string CurrentValue
{
get
{
return mSigGenChannel.CurrentValue.ToString();
}
set
{
mSigGenChannel.CurrentValue = double.Parse(value);
RaisePropertyChanged("CurrentValue");
}
}
public double Frequency
{
get
{
return mSigGenChannel.Frequency;
}
set
{
mSigGenChannel.Frequency = value;
RaisePropertyChanged("Frequency");
}
}
public double Amplitude
{
get
{
return mSigGenChannel.Amplitude;
}
set
{
mSigGenChannel.Amplitude = value;
RaisePropertyChanged("Amplitude");
}
}
public void RefreshValue()
{
//A bit of a cheat, but we provide a means to poke the Viewmodel
//And raise a property change event
RaisePropertyChanged("CurrentValue");
}
also this is the SigChannel model:
class SigGenChannel
{
#region Private members
private UtilityParticipant mParticipant;
private double mCurrentValue;
private double mFrequency;
private double mAmplitude;
private double mTarget;
private double mOffset;
private double mCurrentStepTime;
private DateTime mStartTime;
private System.Threading.Timer mTimer;
private TimerCallback mTCallback;
private int mUpdateInterval = 10;
#endregion
#region Public members
public double CurrentValue
{
get
{
return mCurrentValue;
}
set
{
mCurrentValue = value;
}
}
public double Frequency
{
get
{
return mFrequency;
}
set
{
mFrequency = value;
}
}
public double Amplitude
{
get
{
return mAmplitude;
}
set
{
mAmplitude = value;
}
}
public double Target
{
get
{
return mTarget;
}
set
{
mTarget = value;
}
}
#endregion
//Constructor
public SigGenChannel(UtilityParticipant aParticipant)
{
mParticipant = aParticipant;
mCurrentValue = 10;
mFrequency = 200;
mAmplitude = 100;
mOffset = 0;
mCurrentStepTime = 0;
mStartTime = DateTime.Now;
mTCallback = this.Update;
mTimer = new System.Threading.Timer(mTCallback, null, 500, mUpdateInterval);
//Array enumData = Enum.GetNames;
//RefreshItems();
//Temp Code....!
Collection lCollection = mParticipant.GetCollection("DefaultNodeName.NodeStats");
lCollection.Publish();
}
private void Update(object StateInfo)
{
TimeSpan span = DateTime.Now - mStartTime;
mCurrentStepTime = span.TotalMilliseconds / (double)1000;
mCurrentValue = (Math.Sin(mCurrentStepTime * (mFrequency * 2 * Math.PI)) * mAmplitude / 2) + mOffset;
//Temp Code...!
Collection lCollection = mParticipant.GetCollection("DefaultNodeName.NodeStats");
Parameter lParameter = lCollection.GetParameter("CPUPercent");
lParameter.SetValue(mCurrentValue);
lCollection.Send();
The current code is written in a way that does not follow the WPF suggested practices, and swimming against the current makes things a lot harder than then should be.
What the code should be doing is:
Create a (view)model class for a channel
For example:
class ChannelModel
{
public int Value { get; set; }
public int Frequency { get; set; }
}
Use an ItemsControl instead of a StackPanel
The WPF way of doing things like this is to bind controls to collections, so replace the StackPanel with an ItemsControl.
Bind the ItemsControl to an ObservableCollection of the models
Your main viewmodel should expose an ObservableCollection<ChannelModel> property and the control should bind to that directly:
<ItemsControl ItemsSource="{Binding CollectionOfChannelModels}"/>
This ensures that the control is automatically updated with any changes made to your collection without your needing to do anything else.
Use a DataTemplate to specify how each model should render
So far we 've gotten the control to stay in sync with your channel collection, but we also need to tell it how each item (channel model) should be displayed. To do this, add a DataTemplate to the Resources collection of the ItemsControl:
<ItemsControl.Resources>
<DataTemplate DataType={x:Type local:ChannelModel}>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Value" />
<TextBox Text="{Binding Value}" />
</StackPanel>
</DataTemplate>
</ItemsControl.Resources>
Generally the idea is to create a DataTemplate for a specific type, in your case it is for ViewModel.
Create a DataTemplate for your ViewModel, for instance:
<DataTemplate DataType={x:Type local:ViewModel}>
<TextBox Text="{Binding ViewModelTextProperty}" />
</DataTemplate>
And also in your XAML you must bind your list of ViewModels
<ItemsControl ItemsSource="{Binding myListOfViewModels}"/>
This is going to be straight forward no doubt, but for what ever reason, my mind is drawing a blank on it.
I've got a small, non-resizeable window (325x450) which has 3 Expanders in it, stacked vertically. Each Expander contains an ItemsControl that can potentially have a lot of items in and therefore need to scroll.
What I can't seem to get right is how to layout the Expanders so that they expand to fill any space that is available without pushing other elements off the screen. I can sort of achieve what I'm after by using a Grid and putting each expander in a row with a * height, but this means they are always taking up 1/3 of the window each which defeats the point of the Expander :)
Crappy diagram of what I'm trying to achieve:
This requirement is a little unusal because the you want the state of the Children in the Grid to decide the Height of the RowDefinition they are in.
I really like the layout idea though and I can't believe I never had a similar requirement myself.. :)
For a reusable solution I would use an Attached Behavior for the Grid.
The behavior will subscribe to the Attached Events Expander.Expanded and Expander.Collapsed and in the event handlers, get the right RowDefinition from Grid.GetRow and update the Height accordingly. It works like this
<Grid ex:GridExpanderSizeBehavior.SizeRowsToExpanderState="True">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Expander Grid.Row="0" ... />
<Expander Grid.Row="1" ... />
<Expander Grid.Row="2" ... />
<!-- ... -->
</Grid>
And here is GridExpanderSizeBehavior
public class GridExpanderSizeBehavior
{
public static DependencyProperty SizeRowsToExpanderStateProperty =
DependencyProperty.RegisterAttached("SizeRowsToExpanderState",
typeof(bool),
typeof(GridExpanderSizeBehavior),
new FrameworkPropertyMetadata(false, SizeRowsToExpanderStateChanged));
public static void SetSizeRowsToExpanderState(Grid grid, bool value)
{
grid.SetValue(SizeRowsToExpanderStateProperty, value);
}
private static void SizeRowsToExpanderStateChanged(object target, DependencyPropertyChangedEventArgs e)
{
Grid grid = target as Grid;
if (grid != null)
{
if ((bool)e.NewValue == true)
{
grid.AddHandler(Expander.ExpandedEvent, new RoutedEventHandler(Expander_Expanded));
grid.AddHandler(Expander.CollapsedEvent, new RoutedEventHandler(Expander_Collapsed));
}
else if ((bool)e.OldValue == true)
{
grid.RemoveHandler(Expander.ExpandedEvent, new RoutedEventHandler(Expander_Expanded));
grid.RemoveHandler(Expander.CollapsedEvent, new RoutedEventHandler(Expander_Collapsed));
}
}
}
private static void Expander_Expanded(object sender, RoutedEventArgs e)
{
Grid grid = sender as Grid;
Expander expander = e.OriginalSource as Expander;
int row = Grid.GetRow(expander);
if (row <= grid.RowDefinitions.Count)
{
grid.RowDefinitions[row].Height = new GridLength(1.0, GridUnitType.Star);
}
}
private static void Expander_Collapsed(object sender, RoutedEventArgs e)
{
Grid grid = sender as Grid;
Expander expander = e.OriginalSource as Expander;
int row = Grid.GetRow(expander);
if (row <= grid.RowDefinitions.Count)
{
grid.RowDefinitions[row].Height = new GridLength(1.0, GridUnitType.Auto);
}
}
}
If you don't mind a little code-behind, you could probably hook into the Expanded/Collapsed events, find the parent Grid, get the RowDefinition for the expander, and set the value equal to * if its expanded, or Auto if not.
For example,
Expander ex = sender as Expander;
Grid parent = FindAncestor<Grid>(ex);
int rowIndex = Grid.GetRow(ex);
if (parent.RowDefinitions.Count > rowIndex && rowIndex >= 0)
parent.RowDefinitions[rowIndex].Height =
(ex.IsExpanded ? new GridLength(1, GridUnitType.Star) : GridLength.Auto);
And the FindAncestor method is defined as this:
public static T FindAncestor<T>(DependencyObject current)
where T : DependencyObject
{
// Need this call to avoid returning current object if it is the
// same type as parent we are looking for
current = VisualTreeHelper.GetParent(current);
while (current != null)
{
if (current is T)
{
return (T)current;
}
current = VisualTreeHelper.GetParent(current);
};
return null;
}
I have a DataTable containing an arbitrary number of columns and rows which I am trying to print out. The best luck I've had so far is by putting the data into a Table and then adding the table to a FlowDocument.
So far so good. The problem I have right now is that the Table only "wants" to take up about half of the document's width. I've already set the appropriate values for the FlowDocument's PageWidth and ColumnWidth properties, but the Table doesn't seem to want to stretch to fill up the allotted space?
In order to set your FlowDocument contents to the full available widh you must first know the width of the page. The property you need to set that takes care of the content length is the ColumnWidth prop on the FlowDocument.
I usualy create a "PrintLayout" helper class to keep known presets for the Page width/hight and Padding. Wou can snif presets from Ms Word and fill more.
The class for PrintLayout
public class PrintLayout
{
public static readonly PrintLayout A4 = new PrintLayout("29.7cm", "42cm", "3.18cm", "2.54cm");
public static readonly PrintLayout A4Narrow = new PrintLayout("29.7cm", "42cm", "1.27cm", "1.27cm");
public static readonly PrintLayout A4Moderate = new PrintLayout("29.7cm", "42cm", "1.91cm", "2.54cm");
private Size _Size;
private Thickness _Margin;
public PrintLayout(string w, string h, string leftright, string topbottom)
: this(w,h,leftright, topbottom, leftright, topbottom) {
}
public PrintLayout(string w, string h, string left, string top, string right, string bottom) {
var converter = new LengthConverter();
var width = (double)converter.ConvertFromInvariantString(w);
var height = (double)converter.ConvertFromInvariantString(h);
var marginLeft = (double)converter.ConvertFromInvariantString(left);
var marginTop = (double)converter.ConvertFromInvariantString(top);
var marginRight = (double)converter.ConvertFromInvariantString(right);
var marginBottom = (double)converter.ConvertFromInvariantString(bottom);
this._Size = new Size(width, height);
this._Margin = new Thickness(marginLeft, marginTop, marginRight, marginBottom);
}
public Thickness Margin {
get { return _Margin; }
set { _Margin = value; }
}
public Size Size {
get { return _Size; }
}
public double ColumnWidth {
get {
var column = 0.0;
column = this.Size.Width - Margin.Left - Margin.Right;
return column;
}
}
}
next on your FlowDocument you can set the presets
On Xaml
<FlowDocument x:Class="WpfApp.MyPrintoutView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
PageHeight="{Binding Height, Source={x:Static local:PrintLayout.A4}}"
PageWidth="{Binding Width, Source={x:Static local:PrintLayout.A4}}"
PagePadding="{Binding Margin, Source={x:Static local:PrintLayout.A4}}"
ColumnWidth="{Binding ColumnWidth, Source={x:Static local:PrintLayout.A4}}"
FontFamily="Segoe WP"
FontSize="16" ColumnGap="4">
<!-- flow elements -->
</FlowDocument>
By code
FlowDocument result = new WpfApp.MyPrintoutView();
result.PageWidth = PrintLayout.A4.Size.Width;
result.PageHeight = PrintLayout.A4.Size.Height;
result.PagePadding = PrintLayout.A4.Margin;
result.ColumnWidth = PrintLayout.A4.ColumnWidth;
I had some luck with this: How to set the original width of a WPF FlowDocument, although it only took up about 90% of the space.
I'm not using a listbox and data binding at the moment, but is it possible to have a listbox work like a carousel and if so, how.
This is what I'm using at the moment, which only works for adding images, not through binding in a listbox... can it still be modified to position each binded canvas+image in the suggested answer?
// add images to the stage
public void addImages()
{
var itemCollection = GalleryModel.DocItemCollection;
foreach (var item in itemCollection)
{
var url = item.ImageUrl;
var image = new Image
{
Source = new BitmapImage(new Uri(url, UriKind.RelativeOrAbsolute))
};
image.Width = 90;
image.Height = 60;
// add the image
LayoutRoot.Children.Add(image);
// Add template here?
// reposition the image
posImage(image, itemCollection.IndexOf(item));
_images.Add(image);
var containingWidth = ActualWidth;
var numberofItemsShown = containingWidth/100;
if (itemCollection.IndexOf(item) < Math.Ceiling(numberofItemsShown)-1)
moveIndex(1);
}
}
// move the index
private void moveIndex(int value)
{
_target += value;
_target = Math.Max(0, _target);
_target = Math.Min(_images.Count - 1, _target);
}
// reposition the image
private void posImage(Image image , int index){
double diffFactor = index - _current;
double left = _xCenter - ((IMAGE_WIDTH + OFFSET_FACTOR) * diffFactor);
double top = _yCenter;
image.SetValue(Canvas.LeftProperty, left);
image.SetValue(Canvas.TopProperty, top);
}
You'd typically use a ListBox for scenarios like this.
The XAML for it would look something like this:
<ListBox x:Name="ImageGalleryListBox">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<tkt:WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding MyImageItemUri}" Margin="8" Width="100" Height="100" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
You can of course template it further to make things look the way you want.
In the code-behind here or in your view model, you'd create class that has an MyImageItemUri property and add instances of it to an ObservableCollection<T>. You can then bind or set the collection to the ItemsSource of the ImageGalleryListBox. You'd create more images dynamically by simply adding more of your image items to the observable collection.