diff --git a/.gitignore b/.gitignore
index 70ac09cd..676a854a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
*.user
*.userosscache
*.sln.docstates
+sync.cmd
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
diff --git a/Samples/SampleDeviceCollection/App.config b/Samples/SampleDeviceCollection/App.config
new file mode 100644
index 00000000..88fa4027
--- /dev/null
+++ b/Samples/SampleDeviceCollection/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/SampleDeviceCollection/App.xaml b/Samples/SampleDeviceCollection/App.xaml
new file mode 100644
index 00000000..072cc6c8
--- /dev/null
+++ b/Samples/SampleDeviceCollection/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/Samples/SampleDeviceCollection/App.xaml.cs b/Samples/SampleDeviceCollection/App.xaml.cs
new file mode 100644
index 00000000..f0e468ac
--- /dev/null
+++ b/Samples/SampleDeviceCollection/App.xaml.cs
@@ -0,0 +1,16 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System.Windows;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ }
+}
diff --git a/Samples/SampleDeviceCollection/BooleanConverter.cs b/Samples/SampleDeviceCollection/BooleanConverter.cs
new file mode 100644
index 00000000..57665166
--- /dev/null
+++ b/Samples/SampleDeviceCollection/BooleanConverter.cs
@@ -0,0 +1,105 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace SampleDeviceCollection
+{
+ //-------------------------------------------------------------------
+ // Boolean Converter
+ //-------------------------------------------------------------------
+ #region Boolean Converter
+ ///
+ /// Template allows for easy creation of a value converter for bools
+ ///
+ /// Type to convert back and forth to boolean
+ ///
+ /// See BooleanToVisibilityConverter and BooleanToBrushConverter (below) and usage in Generic.xaml
+ ///
+ public class BooleanConverter : IValueConverter
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of type T that represents true
+ /// The value of type T that represents false
+ public BooleanConverter(T trueValue, T falseValue)
+ {
+ this.True = trueValue;
+ this.False = falseValue;
+ }
+
+ ///
+ /// Gets or sets the value that represents true
+ ///
+ public T True { get; set; }
+
+ ///
+ /// Gets or sets the value that represetns false
+ ///
+ public T False { get; set; }
+
+ ///
+ /// Convert an object of type T to boolean
+ ///
+ /// Object of type T to convert
+ /// The parameter is not used.
+ /// The parameter is not used.
+ /// The parameter is not used.
+ /// Object of boolean value
+ public virtual object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value is bool && ((bool)value) ? this.True : this.False;
+ }
+
+ ///
+ /// Convert a boolean value to an object of type T
+ ///
+ /// The boolean value to convert
+ /// The parameter is not used.
+ /// The parameter is not used.
+ /// The parameter is not used.
+ /// Object of type T
+ public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value is T && EqualityComparer.Default.Equals((T)value, this.True);
+ }
+ }
+
+ ///
+ /// Converter between a boolean and visibility value
+ ///
+ [type: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleClass", Justification = "Small classes are all instances of the same generic and are better organized in a single file.")]
+ public sealed class BooleanToVisibilityConverter : BooleanConverter
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BooleanToVisibilityConverter() :
+ base(Visibility.Visible, Visibility.Hidden)
+ {
+ }
+ }
+
+ ///
+ /// Converter between a boolean and either "http" or "https"
+ ///
+ [type: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleClass", Justification = "Small classes are all instances of the same generic and are better organized in a single file.")]
+ public sealed class BooleanToHttpsConverter : BooleanConverter
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BooleanToHttpsConverter() :
+ base("https", "http")
+ {
+ }
+ }
+ #endregion // Boolean Converter
+}
diff --git a/Samples/SampleDeviceCollection/Controls/AutoScrollTextBox.cs b/Samples/SampleDeviceCollection/Controls/AutoScrollTextBox.cs
new file mode 100644
index 00000000..0372169a
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Controls/AutoScrollTextBox.cs
@@ -0,0 +1,36 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System.Windows;
+using System.Windows.Controls;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// A TextBox derrivative that automatically scrolls to end whenever new text is added.
+ /// This is used for presenting scrolling output spew.
+ ///
+ public class AutoScrollTextBox : TextBox
+ {
+ ///
+ /// Initializes static members of the class.
+ ///
+ static AutoScrollTextBox()
+ {
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(AutoScrollTextBox), new FrameworkPropertyMetadata(typeof(AutoScrollTextBox)));
+ }
+
+ ///
+ /// Override OnTextChanged to automatically scroll to the end
+ ///
+ /// The arguments associated with this event
+ protected override void OnTextChanged(TextChangedEventArgs e)
+ {
+ base.OnTextChanged(e);
+ this.CaretIndex = Text.Length;
+ this.ScrollToEnd();
+ }
+ }
+}
diff --git a/Samples/SampleDeviceCollection/Controls/BindablePassword.xaml b/Samples/SampleDeviceCollection/Controls/BindablePassword.xaml
new file mode 100644
index 00000000..88277aff
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Controls/BindablePassword.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/Samples/SampleDeviceCollection/Controls/BindablePassword.xaml.cs b/Samples/SampleDeviceCollection/Controls/BindablePassword.xaml.cs
new file mode 100644
index 00000000..06e4dfef
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Controls/BindablePassword.xaml.cs
@@ -0,0 +1,73 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System.Security;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// Interaction logic for BindablePassword.xaml
+ ///
+ public partial class BindablePassword : UserControl
+ {
+ //-------------------------------------------------------------------
+ // Constructors
+ //-------------------------------------------------------------------
+ #region Constructors
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BindablePassword()
+ {
+ this.InitializeComponent();
+ }
+ #endregion // Constructors
+
+ //-------------------------------------------------------------------
+ // Dependency Properties
+ //-------------------------------------------------------------------
+ #region Dependency Properties
+ #region Password Dependency Property
+ ///
+ /// Gets or sets the Password dependency property
+ ///
+ public SecureString Password
+ {
+ get
+ {
+ return (SecureString)GetValue(PasswordProperty);
+ }
+
+ set
+ {
+ this.SetValue(PasswordProperty, value);
+ }
+ }
+
+ ///
+ /// Password Dependeny Property static association
+ ///
+ public static readonly DependencyProperty PasswordProperty =
+ DependencyProperty.Register(
+ "Password",
+ typeof(SecureString),
+ typeof(BindablePassword),
+ new PropertyMetadata(default(SecureString)));
+
+ ///
+ /// Forwards change events to the password box's secure string to the Password dependency property
+ ///
+ /// object that originated the event
+ /// Parameters for the event
+ private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
+ {
+ this.Password = ((PasswordBox)sender).SecurePassword;
+ }
+ #endregion // Password Dependency Property
+ #endregion // Dependency Properties
+ }
+}
diff --git a/Samples/SampleDeviceCollection/Controls/DeviceCollectionView.xaml b/Samples/SampleDeviceCollection/Controls/DeviceCollectionView.xaml
new file mode 100644
index 00000000..f1784eba
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Controls/DeviceCollectionView.xaml
@@ -0,0 +1,232 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples/SampleDeviceCollection/Controls/DeviceCollectionView.xaml.cs b/Samples/SampleDeviceCollection/Controls/DeviceCollectionView.xaml.cs
new file mode 100644
index 00000000..078773b5
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Controls/DeviceCollectionView.xaml.cs
@@ -0,0 +1,34 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// Interaction logic for DeviceCollectionView.xaml
+ ///
+ public partial class DeviceCollectionView : UserControl
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DeviceCollectionView()
+ {
+ this.InitializeComponent();
+ }
+
+ ///
+ /// Eats any mouse clicks so that they won't be handled by the ListBox control
+ ///
+ /// Object that originated the event
+ /// Arguments for the event
+ private void EatMouseClicks(object sender, MouseButtonEventArgs e)
+ {
+ e.Handled = true;
+ }
+ }
+}
diff --git a/Samples/SampleDeviceCollection/Controls/DeviceSignInView.xaml b/Samples/SampleDeviceCollection/Controls/DeviceSignInView.xaml
new file mode 100644
index 00000000..d97ec80a
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Controls/DeviceSignInView.xaml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples/SampleDeviceCollection/Controls/DeviceSignInView.xaml.cs b/Samples/SampleDeviceCollection/Controls/DeviceSignInView.xaml.cs
new file mode 100644
index 00000000..7c2a00ca
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Controls/DeviceSignInView.xaml.cs
@@ -0,0 +1,23 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System.Windows.Controls;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// Interaction logic for DeviceSignInView.xaml
+ ///
+ public partial class DeviceSignInView : UserControl
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DeviceSignInView()
+ {
+ this.InitializeComponent();
+ }
+ }
+}
diff --git a/Samples/SampleDeviceCollection/Controls/NumberEntryBox.xaml b/Samples/SampleDeviceCollection/Controls/NumberEntryBox.xaml
new file mode 100644
index 00000000..2ce03c31
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Controls/NumberEntryBox.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples/SampleDeviceCollection/Controls/NumberEntryBox.xaml.cs b/Samples/SampleDeviceCollection/Controls/NumberEntryBox.xaml.cs
new file mode 100644
index 00000000..aa43ffae
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Controls/NumberEntryBox.xaml.cs
@@ -0,0 +1,167 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using Prism.Commands;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// Interaction logic for NumberEntryBox.xaml
+ ///
+ public partial class NumberEntryBox : UserControl, INotifyPropertyChanged
+ {
+ //-------------------------------------------------------------------
+ // Constructors
+ //-------------------------------------------------------------------
+ #region Constructors
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public NumberEntryBox()
+ {
+ this.InitializeComponent();
+ }
+ #endregion // Constructors
+
+ //-------------------------------------------------------------------
+ // Dependency Properties
+ //-------------------------------------------------------------------
+ #region DependencyProperties
+ #region Value Dependency Property
+ ///
+ /// Gets or sets the instance property backing the Value Dependency Property
+ ///
+ public int Value
+ {
+ get { return (int)GetValue(ValueProperty); }
+ set { this.SetValue(ValueProperty, value); }
+ }
+
+ ///
+ /// Value Dependency Property static association
+ ///
+ public static readonly DependencyProperty ValueProperty =
+ DependencyProperty.Register("Value", typeof(int), typeof(NumberEntryBox), new PropertyMetadata(0, OnValueChanged, CoerceValue));
+
+ ///
+ /// Forward the dependency property changes through the INotifyPropertyChanged interface
+ ///
+ /// Dependency opbject that bears the property's value
+ /// The arguments associated with this event
+ private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ NumberEntryBox thisNEB = d as NumberEntryBox;
+ if (thisNEB != null)
+ {
+ thisNEB.PropertyChanged?.Invoke(thisNEB, new PropertyChangedEventArgs("Value"));
+ }
+ }
+
+ ///
+ /// Coerce the value to keep it inside the expected range. The value should not be less than 1
+ ///
+ /// Dependency opbject that bears the property's value
+ /// The value the property would have taken before being coerced
+ /// The coerced value
+ private static object CoerceValue(DependencyObject d, object baseValue)
+ {
+ int val = (int)baseValue > 0 ? (int)baseValue : 1;
+ return val;
+ }
+ #endregion // Value Dependency Property
+ #endregion // Dependency Properties
+
+ //-------------------------------------------------------------------
+ // Commands
+ //-------------------------------------------------------------------
+ #region Commands
+ #region DecrementCommand
+ ///
+ /// The DecrementCommand decrements the number in the Value Depencency Property
+ ///
+ private DelegateCommand decrementCommand;
+
+ ///
+ /// Gets the DecrementCommand
+ ///
+ public ICommand DecrementCommand
+ {
+ get
+ {
+ if (this.decrementCommand == null)
+ {
+ this.decrementCommand = new DelegateCommand(this.ExecuteDecrement, this.CanExecuteDecrement);
+ this.decrementCommand.ObservesProperty(() => this.Value);
+ }
+
+ return this.decrementCommand;
+ }
+ }
+
+ ///
+ /// Predicate for the DecrementCommand
+ ///
+ /// True if the command can be executed
+ private bool CanExecuteDecrement()
+ {
+ return this.Value > 1;
+ }
+
+ ///
+ /// Performs the operation of the DecrementCommand
+ ///
+ private void ExecuteDecrement()
+ {
+ this.Value = this.Value - 1;
+ }
+ #endregion // DecrementCommand
+
+ #region IncrementCommand
+ ///
+ /// The DecrementCommand decrements the number in the Value Depencency Property
+ ///
+ private DelegateCommand incrementCommand;
+
+ ///
+ /// Gets the IncrementCommand
+ ///
+ public ICommand IncrementCommand
+ {
+ get
+ {
+ if (this.incrementCommand == null)
+ {
+ this.incrementCommand = new DelegateCommand(this.ExecuteIncrement);
+ }
+
+ return this.incrementCommand;
+ }
+ }
+
+ ///
+ /// Performs the operation of the Increment command
+ ///
+ private void ExecuteIncrement()
+ {
+ this.Value = this.Value + 1;
+ }
+ #endregion // IncrementCommand
+ #endregion // Commands
+
+ //-------------------------------------------------------------------
+ // INotifyPropertyChanged implementation
+ //-------------------------------------------------------------------
+ #region INotifyPropertyChanged implementation
+ ///
+ /// PropertyChanged event for the INotifyPropertyChanged implementation
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+ #endregion // INotifyPropertyChanged implementation
+ }
+}
diff --git a/Samples/SampleDeviceCollection/Controls/SelectionListBox.cs b/Samples/SampleDeviceCollection/Controls/SelectionListBox.cs
new file mode 100644
index 00000000..08fa4e21
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Controls/SelectionListBox.cs
@@ -0,0 +1,101 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System.Collections;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// Custom ListBox that exposes a SelectionList Dependency property to enable two-way binding.
+ /// Internally, SelectionList is kept in sink with the SelectedItems property of the base class
+ ///
+ public class SelectionListBox : ListBox
+ {
+ //-------------------------------------------------------------------
+ // Constructors
+ //-------------------------------------------------------------------
+ #region Constructors
+ ///
+ /// Initializes static members of the class.
+ ///
+ static SelectionListBox()
+ {
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(SelectionListBox), new FrameworkPropertyMetadata(typeof(SelectionListBox)));
+ }
+ #endregion // Cosntructors
+
+ //-------------------------------------------------------------------
+ // Depenency Properties
+ //-------------------------------------------------------------------
+ #region Dependency Properties
+ #region SelectionList Dependency Property
+
+ ///
+ /// Gets or sets the SelectionList dependency property for the instance of the class
+ ///
+ public IList SelectionList
+ {
+ get { return (IList)GetValue(SelectionListProperty); }
+ set { this.SetValue(SelectionListProperty, value); }
+ }
+
+ ///
+ /// SelectionList Dependency Property static association
+ ///
+ public static readonly DependencyProperty SelectionListProperty =
+ DependencyProperty.Register("SelectionList", typeof(IList), typeof(SelectionListBox), new PropertyMetadata(null, null, CoerceSelectionList));
+
+ ///
+ /// Coerce the value of SelectionList so that it is identical to (i.e. the same object as) the
+ /// value of SelectedItems
+ ///
+ /// SelectionListBox instance
+ /// New list of selected items
+ /// The list of selected items of the list
+ private static object CoerceSelectionList(DependencyObject d, object baseValue)
+ {
+ SelectionListBox thisSLB = d as SelectionListBox;
+ IList selectedItems = thisSLB.SelectedItems;
+ IList baseList = baseValue as IList;
+ if (baseList == selectedItems)
+ {
+ // Must have been called from OnSelectionChanged...
+ // ...implies nothing to do so early out
+ thisSLB.OnPropertyChanged(new DependencyPropertyChangedEventArgs(SelectionListProperty, null, selectedItems));
+ return selectedItems;
+ }
+
+ // baseValue is not identical to SelectedItems, so fixup SelectedItems
+ // to have the same elements as baseValue...
+ // Note: This will result in multiple calls to OnSelectionChanged which
+ // will subsequently call this method but will early out according to
+ // the condition described above.
+ thisSLB.SelectedItems.Clear();
+ if (baseList != null)
+ {
+ foreach (object itm in baseList)
+ {
+ thisSLB.SelectedItems.Add(itm);
+ }
+ }
+
+ return selectedItems;
+ }
+
+ ///
+ /// OnSelectionChange needs to drive changes to the SelectionList Dependency Property
+ ///
+ /// The arguments associated with this event
+ protected override void OnSelectionChanged(SelectionChangedEventArgs e)
+ {
+ base.OnSelectionChanged(e);
+ this.SetValue(SelectionListProperty, this.SelectedItems);
+ }
+ #endregion // SelectionList Dependency Property
+ #endregion // Dependency Properties
+ }
+}
diff --git a/Samples/SampleDeviceCollection/MainWindow.xaml b/Samples/SampleDeviceCollection/MainWindow.xaml
new file mode 100644
index 00000000..5f1cb3c6
--- /dev/null
+++ b/Samples/SampleDeviceCollection/MainWindow.xaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples/SampleDeviceCollection/MainWindow.xaml.cs b/Samples/SampleDeviceCollection/MainWindow.xaml.cs
new file mode 100644
index 00000000..c049e37f
--- /dev/null
+++ b/Samples/SampleDeviceCollection/MainWindow.xaml.cs
@@ -0,0 +1,23 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System.Windows;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MainWindow()
+ {
+ this.InitializeComponent();
+ }
+ }
+}
diff --git a/Samples/SampleDeviceCollection/Properties/AssemblyInfo.cs b/Samples/SampleDeviceCollection/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..4ff94abf
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Properties/AssemblyInfo.cs
@@ -0,0 +1,56 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("SampleDeviceCollection")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("SampleDeviceCollection")]
+[assembly: AssemblyCopyright("Copyright © 2016")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+//In order to begin building localizable applications, set
+//CultureYouAreCodingWith in your .csproj file
+//inside a . For example, if you are using US english
+//in your source files, set the to en-US. Then uncomment
+//the NeutralResourceLanguage attribute below. Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+////[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Samples/SampleDeviceCollection/Properties/Resources.Designer.cs b/Samples/SampleDeviceCollection/Properties/Resources.Designer.cs
new file mode 100644
index 00000000..e3e69d2f
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Properties/Resources.Designer.cs
@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace SampleDeviceCollection.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SampleDeviceCollection.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/Samples/SampleDeviceCollection/Properties/Resources.resx b/Samples/SampleDeviceCollection/Properties/Resources.resx
new file mode 100644
index 00000000..af7dbebb
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Properties/Resources.resx
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Samples/SampleDeviceCollection/Properties/Settings.Designer.cs b/Samples/SampleDeviceCollection/Properties/Settings.Designer.cs
new file mode 100644
index 00000000..4869e044
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Properties/Settings.Designer.cs
@@ -0,0 +1,26 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace SampleDeviceCollection.Properties {
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default {
+ get {
+ return defaultInstance;
+ }
+ }
+ }
+}
diff --git a/Samples/SampleDeviceCollection/Properties/Settings.settings b/Samples/SampleDeviceCollection/Properties/Settings.settings
new file mode 100644
index 00000000..033d7a5e
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Properties/Settings.settings
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/SampleDeviceCollection/SampleDeviceCollection.csproj b/Samples/SampleDeviceCollection/SampleDeviceCollection.csproj
new file mode 100644
index 00000000..50b51a15
--- /dev/null
+++ b/Samples/SampleDeviceCollection/SampleDeviceCollection.csproj
@@ -0,0 +1,164 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {C0C9CBEF-E460-431B-90DD-4B1EFEBB9445}
+ WinExe
+ Properties
+ SampleDeviceCollection
+ SampleDeviceCollection
+ v4.5.2
+ 512
+ {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ 4
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ packages\Prism.Core.6.1.0\lib\net45\Prism.dll
+ True
+
+
+
+
+
+
+
+
+
+
+ 4.0
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+
+
+
+
+
+ DeviceCollectionView.xaml
+
+
+
+
+ DeviceSignInView.xaml
+
+
+
+ NumberEntryBox.xaml
+
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ App.xaml
+ Code
+
+
+ BindablePassword.xaml
+
+
+
+
+ MainWindow.xaml
+ Code
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+
+
+ Code
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ Settings.settings
+ True
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+
+
+
+
+
+
+ {6a9e862e-5cda-4a8a-bbc0-56e9ea921e39}
+ WindowsDevicePortalWrapper
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/SampleDeviceCollection/SampleDeviceCollection.sln b/Samples/SampleDeviceCollection/SampleDeviceCollection.sln
new file mode 100644
index 00000000..8ab2951c
--- /dev/null
+++ b/Samples/SampleDeviceCollection/SampleDeviceCollection.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.25420.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleDeviceCollection", "SampleDeviceCollection.csproj", "{C0C9CBEF-E460-431B-90DD-4B1EFEBB9445}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsDevicePortalWrapper", "..\..\WindowsDevicePortalWrapper\WindowsDevicePortalWrapper\WindowsDevicePortalWrapper.csproj", "{6A9E862E-5CDA-4A8A-BBC0-56E9EA921E39}"
+EndProject
+Global
+ GlobalSection(SharedMSBuildProjectFiles) = preSolution
+ ..\..\WindowsDevicePortalWrapper\WindowsDevicePortalWrapper.Shared\WindowsDevicePortalWrapper.Shared.projitems*{6a9e862e-5cda-4a8a-bbc0-56e9ea921e39}*SharedItemsImports = 4
+ EndGlobalSection
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C0C9CBEF-E460-431B-90DD-4B1EFEBB9445}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C0C9CBEF-E460-431B-90DD-4B1EFEBB9445}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C0C9CBEF-E460-431B-90DD-4B1EFEBB9445}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C0C9CBEF-E460-431B-90DD-4B1EFEBB9445}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6A9E862E-5CDA-4A8A-BBC0-56E9EA921E39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6A9E862E-5CDA-4A8A-BBC0-56E9EA921E39}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6A9E862E-5CDA-4A8A-BBC0-56E9EA921E39}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6A9E862E-5CDA-4A8A-BBC0-56E9EA921E39}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/Samples/SampleDeviceCollection/Settings.StyleCop b/Samples/SampleDeviceCollection/Settings.StyleCop
new file mode 100644
index 00000000..c7a638bf
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Settings.StyleCop
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+
+
+
+
+
+ False
+
+
+
+
+
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+ False
+
+
+
+
+
+
+
+
+
+ False
+
+
+
+
+
+
+
+
+ un
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/SampleDeviceCollection/Themes/CustomStyles.xaml b/Samples/SampleDeviceCollection/Themes/CustomStyles.xaml
new file mode 100644
index 00000000..0cb24049
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Themes/CustomStyles.xaml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/SampleDeviceCollection/Themes/Generic.xaml b/Samples/SampleDeviceCollection/Themes/Generic.xaml
new file mode 100644
index 00000000..e621fb66
--- /dev/null
+++ b/Samples/SampleDeviceCollection/Themes/Generic.xaml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/Samples/SampleDeviceCollection/ViewModels/CommandSequence.cs b/Samples/SampleDeviceCollection/ViewModels/CommandSequence.cs
new file mode 100644
index 00000000..b196e4cb
--- /dev/null
+++ b/Samples/SampleDeviceCollection/ViewModels/CommandSequence.cs
@@ -0,0 +1,236 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System;
+using System.Collections.Generic;
+using System.Windows.Input;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// Composes several ICommand implementations to run in sequence with each command executing
+ /// as soon as the command's predicate is satisfied. Can be used to construct sophisticated
+ /// commands by sequencing several smaller commands.
+ ///
+ public class CommandSequence : ICommand
+ {
+ //-------------------------------------------------------------------
+ // Constructor
+ //-------------------------------------------------------------------
+ #region Constructor
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// Multiple CommandSequences may share the same ObservableCommandQueue.
+ /// The predicate for the CommandSequence will return fale in the case
+ /// when another command sequence is using the shared queue
+ ///
+ public CommandSequence(ObservableCommandQueue commandQueue = null)
+ {
+ this.registeredCommands = new List();
+ this.commandQueue = commandQueue == null ? new ObservableCommandQueue() : commandQueue;
+ this.commandQueue.QueueChanged += this.CommandQueue_QueueChanged;
+ }
+ #endregion // Constructor
+
+ //-------------------------------------------------------------------
+ // Private Class Members
+ //-------------------------------------------------------------------
+ #region Private Class Members
+ ///
+ /// The ICommand instances that are composed together in this CommandSequence
+ ///
+ private List registeredCommands;
+
+ ///
+ /// The CommandSequence is executed by first placing all the commands into a queue
+ /// and then executing each command as it becomes ready
+ ///
+ private ObservableCommandQueue commandQueue;
+
+ ///
+ /// The parameter passed to execute and subsequently shared with each command when it executes
+ ///
+ private object sharedParameter;
+ #endregion // Private Class Members
+
+ //-------------------------------------------------------------------
+ // Command Registration
+ //-------------------------------------------------------------------
+ #region Command Registration
+ ///
+ /// Register a command with the CommandSequence
+ /// Commands are composed in the same order that they are registered
+ ///
+ /// Command to register with this CommandSequence
+ public void RegisterCommand(ICommand cmd)
+ {
+ if (cmd == null)
+ {
+ throw new ArgumentException(nameof(cmd));
+ }
+
+ if (cmd == this)
+ {
+ throw new ArgumentException("Cannot register a CommandSequence with itself");
+ }
+
+ lock (this.registeredCommands)
+ {
+ CommandSequence seq = cmd as CommandSequence;
+ if (seq == null)
+ {
+ this.AddCommand(cmd);
+ }
+ else
+ {
+ // If the command is itself a CommandSequence then we crack it open
+ // and add the individual commands within it. This enables composing
+ // commands with other CommandSequences. Flattening is required in
+ // the case when multiple command sequences share the same queue
+ foreach (ICommand subcmd in seq.registeredCommands)
+ {
+ this.AddCommand(subcmd);
+ }
+ }
+ }
+
+ this.OnCanExecuteChanged();
+ }
+
+ ///
+ /// Internal helper to add a command to the list of commands.
+ ///
+ /// Command to add to this CommandSequence
+ private void AddCommand(ICommand cmd)
+ {
+ if (this.registeredCommands.Count == 0)
+ {
+ cmd.CanExecuteChanged += this.Forward_CanExecuteChanged;
+ }
+
+ this.registeredCommands.Add(cmd);
+ }
+ #endregion // Command Registration
+
+ //-------------------------------------------------------------------
+ // ICommand Implementation
+ //-------------------------------------------------------------------
+ #region ICommand Implementation
+ #region CanExecuteChanged event
+ ///
+ /// Event signals when the ability to execute the CommandSequence has changed
+ ///
+ public event EventHandler CanExecuteChanged;
+
+ ///
+ /// Invoke the CanExecuteChanged event handler
+ ///
+ private void OnCanExecuteChanged()
+ {
+ this.CanExecuteChanged?.Invoke(this, new EventArgs());
+ }
+
+ ///
+ /// Forward on the CanExecuteChanged event from the first command in the list.
+ /// See CanExecute for the conditions under which the CommandSequence can execute.
+ ///
+ /// Originator of the event
+ /// The arguments associated with this event
+ private void Forward_CanExecuteChanged(object sender, EventArgs e)
+ {
+ this.CanExecuteChanged?.Invoke(sender, e);
+ }
+ #endregion // CanExecuteChanged event
+
+ ///
+ /// Predicate indicates whether the CommandSequence is ready to execute
+ ///
+ /// Value passed to Execute/CanExecute for the commands in the sequence
+ /// Indicates whether this CommandSequence is ready to execute
+ public bool CanExecute(object parameter)
+ {
+ return
+ this.registeredCommands.Count > 0 // Must have at least one command to execute
+ && this.commandQueue.Count == 0 // The queue must not be in use already
+ && this.registeredCommands[0].CanExecute(parameter); // First command must be ready to execute
+ }
+
+ ///
+ /// Execute each command in the CommandSequence as they become ready
+ ///
+ /// The command parameter to be passed to all of the commands
+ public void Execute(object parameter)
+ {
+ lock (this.commandQueue)
+ {
+ if (this.registeredCommands.Count > 0 && this.commandQueue.Count == 0)
+ {
+ foreach (ICommand cmd in this.registeredCommands)
+ {
+ this.commandQueue.Enqueue(cmd);
+ this.sharedParameter = parameter;
+ }
+ }
+ else
+ {
+ return;
+ }
+ }
+
+ this.OnCanExecuteChanged();
+ this.ExecuteNext();
+ }
+
+ ///
+ /// Event handler used determine when the command at the head of the queue is ready to execute
+ ///
+ /// The originator of the event should be the command at the front of the queue
+ /// The arguments associated with this event
+ private void CurrentCommand_CanExecuteChanged(object sender, EventArgs e)
+ {
+ ICommand cmd = sender as ICommand;
+ if (cmd.CanExecute(this.sharedParameter))
+ {
+ cmd.CanExecuteChanged -= this.CurrentCommand_CanExecuteChanged;
+ this.ExecuteNext();
+ }
+ }
+
+ ///
+ /// Execute all the commands from the front of the queue that are already ready then hook up the
+ /// event to receive the signal when the next one is ready.
+ ///
+ private void ExecuteNext()
+ {
+ lock (this.commandQueue)
+ {
+ while (this.commandQueue.Count > 0 && this.commandQueue.Peek().CanExecute(this.sharedParameter))
+ {
+ ICommand cmd = this.commandQueue.Dequeue();
+ cmd.Execute(this.sharedParameter);
+ }
+
+ if (this.commandQueue.Count > 0)
+ {
+ this.commandQueue.Peek().CanExecuteChanged += this.CurrentCommand_CanExecuteChanged;
+ }
+ }
+ }
+
+ ///
+ /// The ObservableCommandQueue informs the CommandSequence when queue contents change so
+ /// that the CommandSequence can fire the CanExecuteChanged.
+ ///
+ /// ObservableCommandQueue that originated the event
+ /// The arguments associated with this event
+ private void CommandQueue_QueueChanged(object sender, EventArgs e)
+ {
+ this.OnCanExecuteChanged();
+ }
+ #endregion // ICommand Implementation
+ }
+}
diff --git a/Samples/SampleDeviceCollection/ViewModels/DevicePortalCommandModel.cs b/Samples/SampleDeviceCollection/ViewModels/DevicePortalCommandModel.cs
new file mode 100644
index 00000000..f8a2f1c2
--- /dev/null
+++ b/Samples/SampleDeviceCollection/ViewModels/DevicePortalCommandModel.cs
@@ -0,0 +1,242 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System;
+using Microsoft.Tools.WindowsDevicePortal;
+using Prism.Mvvm;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// Base class for ViewModel classes that wrap a DevicePortal object.
+ /// Provides status for pending DevicePortal operations, diagnostic
+ /// output support, an ObservableCommandQueue to enable composing
+ /// commands using CommandSequences, etc.
+ ///
+ public class DevicePortalCommandModel : BindableBase
+ {
+ //-------------------------------------------------------------------
+ // Constructors
+ //-------------------------------------------------------------------
+ #region Constructors
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// IDevicePortalConnection object used for connecting
+ /// Diagnostic sink for reporting
+ public DevicePortalCommandModel(IDevicePortalConnection connection, IDiagnosticSink diags)
+ {
+ if (connection == null)
+ {
+ throw new ArgumentException("Must provide a valid IDevicePortalConnection object");
+ }
+
+ this.Connection = connection;
+ this.Portal = new DevicePortal(connection);
+
+ this.diagnostics = diags;
+ this.commandQueue = new ObservableCommandQueue();
+ this.Ready = true;
+ }
+ #endregion // Constructors
+
+ //-------------------------------------------------------------------
+ // Class Members
+ //-------------------------------------------------------------------
+ #region Class Members
+ ///
+ /// Gets or sets the DevicePortal object encapsulated by this class
+ ///
+ public DevicePortal Portal { get; protected set; }
+
+ ///
+ /// Gets or sets the IDevicePortalConnection object encapsulated by this class
+ ///
+ public IDevicePortalConnection Connection { get; protected set; }
+ #endregion // Class Members
+
+ //-------------------------------------------------------------------
+ // Properties
+ //-------------------------------------------------------------------
+ #region Properties
+ #region Ready
+ ///
+ /// The Ready property indicates that there are no RESTful calls in flight via
+ /// the underlying DevicePortal object
+ ///
+ private bool ready;
+
+ ///
+ /// Gets or sets a value indicating whether the DevicePortal is ready
+ ///
+ public bool Ready
+ {
+ get
+ {
+ return this.ready;
+ }
+
+ protected set
+ {
+ this.SetProperty(ref this.ready, value);
+ }
+ }
+ #endregion // Ready
+
+ #region Address
+ ///
+ /// Gets the IP Address for the device
+ ///
+ public string Address
+ {
+ get
+ {
+ return this.Portal == null ? "" : this.Portal.Address.Split(':')[0];
+ }
+ }
+ #endregion // Address
+
+ #region DeviceFamily
+ ///
+ /// Gets the device's DeviceFamily
+ ///
+ public string DeviceFamily
+ {
+ get
+ {
+ return this.Portal == null ? "" : this.Portal.DeviceFamily;
+ }
+ }
+ #endregion // DeviceFamily
+
+ #region OperatingSystemVersion
+ ///
+ /// Gets the operating system version for the device
+ ///
+ public string OperatingSystemVersion
+ {
+ get
+ {
+ return this.Portal == null ? "" : this.Portal.OperatingSystemVersion;
+ }
+ }
+ #endregion // OperatingSystemVersion
+
+ #region Platform
+ ///
+ /// Gets the platform for the device
+ ///
+ public string Platform
+ {
+ get
+ {
+ return this.Portal == null ? "" : this.Portal.Platform.ToString();
+ }
+ }
+ #endregion // Platform
+
+ #region PlatformName
+ ///
+ /// Gets the platform name of the device
+ ///
+ public string PlatformName
+ {
+ get
+ {
+ return this.Portal == null ? "" : this.Portal.PlatformName;
+ }
+ }
+ #endregion // PlatformName
+ #endregion // Properties
+
+ //-------------------------------------------------------------------
+ // Command Sequences
+ //-------------------------------------------------------------------
+ #region Command Sequences
+ ///
+ /// CommandQueue shared by all command sequences created via a call to CreateCommandSequence.
+ /// CommandSequences will not be able to execute in the sense of CanExecute if there is already
+ /// another command sequence usign the shared queue
+ ///
+ private ObservableCommandQueue commandQueue;
+
+ ///
+ /// Clears the shared command queue
+ ///
+ public void ClearCommandQueue()
+ {
+ this.commandQueue.Clear();
+ }
+
+ ///
+ /// Creates a new CommandSequence using the shared command queue
+ ///
+ /// The new CommandSequence
+ public CommandSequence CreateCommandSequence()
+ {
+ return new CommandSequence(this.commandQueue);
+ }
+ #endregion // Command Sequences
+
+ //-------------------------------------------------------------------
+ // Diagnostics
+ //-------------------------------------------------------------------
+ #region Diagnostics
+ ///
+ /// Destination for reporting diagnostic messages
+ ///
+ private IDiagnosticSink diagnostics;
+
+ ///
+ /// Gets protected access to the diagnostic sink
+ ///
+ protected IDiagnosticSink Diagnostics
+ {
+ get
+ {
+ return this.diagnostics;
+ }
+ }
+
+ ///
+ /// Gets a short string, identifying the device, that is prepended to diagnostic output
+ ///
+ protected virtual string DiagnosticMoniker
+ {
+ get
+ {
+ return this.Address;
+ }
+ }
+
+ ///
+ /// Output diagnostic messages with the moniker already prepended to the output
+ ///
+ /// Format string
+ /// Format arguments
+ protected virtual void OutputDiagnosticString(string fmt, params object[] args)
+ {
+ this.diagnostics.OutputDiagnosticString("[{0}] ", this.DiagnosticMoniker);
+ this.diagnostics.OutputDiagnosticString(fmt, args);
+ }
+
+ ///
+ /// Report an exception to the diagnostic output and clear the command queue
+ ///
+ /// The command that generated the exception
+ /// The exception caught when attempting to execute the command
+ protected virtual void ReportException(string commandName, Exception exn)
+ {
+ this.OutputDiagnosticString("Exception during {0} command:\n", commandName);
+ this.OutputDiagnosticString(exn.Message + "\n");
+ this.OutputDiagnosticString(exn.StackTrace + "\n");
+
+ // Clear the command queue to prevent executing any more commands
+ this.OutputDiagnosticString("Clearing any queued commands\n");
+ this.ClearCommandQueue();
+ }
+ #endregion // Diagnostics
+ }
+}
diff --git a/Samples/SampleDeviceCollection/ViewModels/DevicePortalViewModel.cs b/Samples/SampleDeviceCollection/ViewModels/DevicePortalViewModel.cs
new file mode 100644
index 00000000..86def849
--- /dev/null
+++ b/Samples/SampleDeviceCollection/ViewModels/DevicePortalViewModel.cs
@@ -0,0 +1,780 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System;
+using System.Net;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+using Microsoft.Tools.WindowsDevicePortal;
+using Prism.Commands;
+using static Microsoft.Tools.WindowsDevicePortal.DevicePortal;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// Viewmodel for a single connected device
+ ///
+ public class DevicePortalViewModel : DevicePortalCommandModel
+ {
+ //-------------------------------------------------------------------
+ // Constructors
+ //-------------------------------------------------------------------
+ #region Constructors
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// IDevicePortalConnection used for connecting
+ /// Diagnostic sink for reporting
+ public DevicePortalViewModel(IDevicePortalConnection connection, IDiagnosticSink diags)
+ : base(connection, diags)
+ {
+ this.connectionStatus = DeviceConnectionStatus.None;
+
+ // Add additional handling for untrusted certs.
+ this.Portal.UnvalidatedCert += this.DoCertValidation;
+
+ // Default number of retry attempts to make when reestablishing the connection
+ this.ConnectionRetryAttempts = 3;
+ }
+ #endregion // Cosntructors
+
+ //-------------------------------------------------------------------
+ // Properties
+ //-------------------------------------------------------------------
+ #region Properties
+ #region Diagnostic Moniker
+ ///
+ /// Gets the overriden DiagnosticMoniker for the device. The overriden
+ /// version will use the friendly name for the device or otherwise
+ /// default to the IP address.
+ ///
+ protected override string DiagnosticMoniker
+ {
+ get
+ {
+ return !string.IsNullOrWhiteSpace(this.DeviceName) ? this.DeviceName : base.DiagnosticMoniker;
+ }
+ }
+ #endregion // Diagnostic Moniker
+
+ #region DeviceName
+ ///
+ /// The friendly name for the device
+ ///
+ private string deviceName;
+
+ ///
+ /// Gets the freindly name for the device
+ ///
+ public string DeviceName
+ {
+ get
+ {
+ return string.IsNullOrWhiteSpace(this.deviceName) ? "" : this.deviceName;
+ }
+
+ private set
+ {
+ this.SetProperty(ref this.deviceName, value);
+ }
+ }
+ #endregion //DeviceName
+
+ #region DeviceNameEntry
+ ///
+ /// User input from the user to be used as a new device name
+ ///
+ private string deviceNameEntry;
+
+ ///
+ /// Gets or sets a value representing input from the user to be used as a new device name
+ ///
+ public string DeviceNameEntry
+ {
+ get
+ {
+ return this.deviceNameEntry;
+ }
+
+ set
+ {
+ this.SetProperty(ref this.deviceNameEntry, value);
+ }
+ }
+ #endregion // DeviceNameEntry
+
+ #region CPULoad
+ ///
+ /// The CPU Load on the device
+ ///
+ private int cpuLoad;
+
+ ///
+ /// Gets a value representing the CPU load on the device
+ ///
+ public string CPULoad
+ {
+ get
+ {
+ return this.cpuLoad.ToString();
+ }
+ }
+ #endregion // CPULoad
+
+ #region ConnectionRetryAttempts
+ ///
+ /// The number of times to attempt to connect to the device
+ ///
+ private int connectionRetryAttempts;
+
+ ///
+ /// Gets or sets the number of time to attempt to connect to the device
+ ///
+ public int ConnectionRetryAttempts
+ {
+ get
+ {
+ return this.connectionRetryAttempts;
+ }
+
+ set
+ {
+ this.SetProperty(ref this.connectionRetryAttempts, value);
+ }
+ }
+ #endregion // ConnectionRetryAttempts
+
+ #region ConnectionStatus
+ ///
+ /// Status of the connection associated with this DevicePortalViewModel
+ ///
+ private DeviceConnectionStatus connectionStatus;
+
+ ///
+ /// Gets the status of the connection associated with this DevicePortalViewModel
+ ///
+ public DeviceConnectionStatus ConnectionStatus
+ {
+ get
+ {
+ return this.connectionStatus;
+ }
+
+ private set
+ {
+ this.SetProperty(ref this.connectionStatus, value);
+ }
+ }
+
+ #endregion // ConnectionStatus
+ #endregion // Properties
+
+ //-------------------------------------------------------------------
+ // Commands
+ //-------------------------------------------------------------------
+ #region Commands
+
+ #region DumpIpConfigCommand
+ ///
+ /// Command to dump the IP configuration for the remote device
+ ///
+ private CommandSequence dumpIpConfigCommand;
+
+ ///
+ /// Gets the command to dump the IP configuration for the remote device
+ ///
+ public ICommand DumpIpConfigCommand
+ {
+ get
+ {
+ if (this.dumpIpConfigCommand == null)
+ {
+ this.dumpIpConfigCommand = this.CreateCommandSequence();
+ DelegateCommand dumpIpConfigDC = DelegateCommand.FromAsyncHandler(this.ExecuteDumpIpConfigAsync, this.CanExecuteDumpIpConfig);
+ dumpIpConfigDC.ObservesProperty(() => this.Ready);
+ this.dumpIpConfigCommand.RegisterCommand(dumpIpConfigDC);
+ }
+
+ return this.dumpIpConfigCommand;
+ }
+ }
+
+ ///
+ /// Predicate for the DumpIpConfigCommand
+ ///
+ /// Result indicates whether the command can execute
+ private bool CanExecuteDumpIpConfig()
+ {
+ return this.Ready;
+ }
+
+ ///
+ /// Performs the action for the DumpIPConfigCommand
+ ///
+ /// A task capturing the continuation of dumping the IP config
+ private async Task ExecuteDumpIpConfigAsync()
+ {
+ this.OutputDiagnosticString("ExecuteDumpIpConfigAsync\n");
+ this.Ready = false;
+ try
+ {
+ IpConfiguration config = await this.Portal.GetIpConfigAsync();
+ this.OutputIpConfiguration(config);
+ }
+ catch (Exception exn)
+ {
+ this.ReportException("DumpIPConfig", exn);
+ }
+
+ this.Ready = true;
+ }
+
+ ///
+ /// Writes an IP configuration to the diagnostic output
+ ///
+ /// The IP configuration to write out
+ private void OutputIpConfiguration(IpConfiguration config)
+ {
+ // For now, just dump out the adapters to the debug output to see what I got
+ this.OutputDiagnosticString("Dumping network adapter information:\n");
+
+ foreach (NetworkAdapterInfo nai in config.Adapters)
+ {
+ this.OutputDiagnosticString("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
+ this.OutputDiagnosticString(" Description: {0}\n", nai.Description);
+ this.OutputDiagnosticString(" Mac Address: {0}\n", nai.MacAddress);
+ this.OutputDiagnosticString(" Index: {0}\n", nai.Index);
+ this.OutputDiagnosticString(" Id: {0}\n", nai.Id);
+ this.OutputDiagnosticString(" Adapter Type: {0}\n", nai.AdapterType);
+ this.OutputDiagnosticString(" DHCP:\n");
+ this.OutputDHCPInfo(nai.Dhcp);
+ this.OutputDiagnosticString(" Gateways:\n");
+ foreach (IpAddressInfo iai in nai.Gateways)
+ {
+ this.OutputIpAddressInfo(iai);
+ }
+
+ this.OutputDiagnosticString(" IP Addresses:\n");
+ foreach (IpAddressInfo iai in nai.IpAddresses)
+ {
+ this.OutputIpAddressInfo(iai);
+ }
+ }
+ }
+
+ ///
+ /// Writes DHCP configuration data to the diagnostic output
+ ///
+ /// The DHCP configuration data to write
+ private void OutputDHCPInfo(Dhcp dhcp)
+ {
+ this.OutputIpAddressInfo(dhcp.Address);
+ this.OutputDiagnosticString(" Lease Obtained {0}\n", dhcp.LeaseObtained.ToLocalTime().ToString());
+ this.OutputDiagnosticString(" Lease Expires {0}\n", dhcp.LeaseExpires.ToLocalTime().ToString());
+ }
+
+ ///
+ /// Writes IP address information to the diagnostic output
+ ///
+ /// The IP address information to write
+ private void OutputIpAddressInfo(IpAddressInfo ipAddr)
+ {
+ this.OutputDiagnosticString(" Address: {0}\n", ipAddr.Address);
+ this.OutputDiagnosticString(" Subnet Mask: {0}\n", ipAddr.SubnetMask);
+ }
+ #endregion // DumpIpConfigCommand
+
+ #region RenameCommand
+ ///
+ /// Command to rename the device
+ ///
+ private CommandSequence renameCommand;
+
+ ///
+ /// Gets the command to rename the device
+ ///
+ public ICommand RenameCommand
+ {
+ get
+ {
+ if (this.renameCommand == null)
+ {
+ this.renameCommand = this.CreateCommandSequence();
+ DelegateCommand renameDC = DelegateCommand.FromAsyncHandler(this.ExecuteRenameAsync, this.CanExecuteRename);
+ renameDC.ObservesProperty(() => this.Ready);
+ renameDC.ObservesProperty(() => this.DeviceNameEntry);
+
+ this.renameCommand.RegisterCommand(renameDC);
+ this.renameCommand.RegisterCommand(this.ReestablishConnectionCommand);
+ this.renameCommand.RegisterCommand(this.RebootCommand);
+ }
+
+ return this.renameCommand;
+ }
+ }
+
+ ///
+ /// Predicate for the rename command
+ ///
+ /// Result indicates whether the rename command can execute
+ private bool CanExecuteRename()
+ {
+ return
+ this.Ready &&
+ !string.IsNullOrWhiteSpace(this.deviceNameEntry);
+ }
+
+ ///
+ /// Performs the action for the rename command
+ ///
+ /// A task capturing the continuation of setting the device name
+ private async Task ExecuteRenameAsync()
+ {
+ this.OutputDiagnosticString("ExecuteRenameAsync\n");
+ this.Ready = false;
+ try
+ {
+ string newName = this.deviceNameEntry;
+ this.DeviceNameEntry = string.Empty;
+ this.OutputDiagnosticString("Attempting to rename device to {0}\n", newName);
+ await this.Portal.SetDeviceNameAsync(newName);
+ }
+ catch (Exception exn)
+ {
+ this.ReportException("Rename", exn);
+ }
+
+ this.Ready = true;
+ }
+
+ #endregion // RenameCommand
+
+ #region RefreshDeviceName
+ ///
+ /// Retreive the friendly device name from the device
+ ///
+ private CommandSequence refreshDeviceNameCommand;
+
+ ///
+ /// Gets the command for retrieving the device name
+ ///
+ public ICommand RefreshDeviceNameCommand
+ {
+ get
+ {
+ if (this.refreshDeviceNameCommand == null)
+ {
+ DelegateCommand refreshDeviceNameDC = DelegateCommand.FromAsyncHandler(this.RefreshDeviceNameAsync, this.CanExecuteRefreshDeviceName);
+ refreshDeviceNameDC.ObservesProperty(() => this.Ready);
+ this.refreshDeviceNameCommand = this.CreateCommandSequence();
+ this.refreshDeviceNameCommand.RegisterCommand(refreshDeviceNameDC);
+ }
+
+ return this.refreshDeviceNameCommand;
+ }
+ }
+
+ ///
+ /// Predicate indicates whether refresh device name command can execute
+ ///
+ /// Whether the command can execute
+ private bool CanExecuteRefreshDeviceName()
+ {
+ return this.Ready;
+ }
+
+ ///
+ /// Performs the action of the refresh device name command
+ ///
+ /// A task that captures the continuation of the refresh device name action
+ private async Task RefreshDeviceNameAsync()
+ {
+ this.OutputDiagnosticString("RefreshDeviceNameAsync\n");
+ this.Ready = false;
+ try
+ {
+ this.DeviceName = await this.Portal.GetDeviceNameAsync();
+ this.OutputDiagnosticString("Done refreshing device name\n");
+ }
+ catch (Exception exn)
+ {
+ this.ReportException("RefreshDeviceName", exn);
+ }
+
+ this.Ready = true;
+ }
+ #endregion // RefreshDeviceName
+
+ #region Reboot Command
+ ///
+ /// Command to reboot the device
+ ///
+ private CommandSequence rebootCommand;
+
+ ///
+ /// Gets the command for rebooting the device
+ ///
+ public ICommand RebootCommand
+ {
+ get
+ {
+ if (this.rebootCommand == null)
+ {
+ this.rebootCommand = this.CreateCommandSequence();
+ DelegateCommand rebootDC = DelegateCommand.FromAsyncHandler(this.ExecuteRebootAsync, this.CanExecuteReboot);
+ rebootDC.ObservesProperty(() => this.Ready);
+ this.rebootCommand.RegisterCommand(this.StopListeningForSystemPerfCommand);
+ this.rebootCommand.RegisterCommand(rebootDC);
+ this.rebootCommand.RegisterCommand(this.ReestablishConnectionCommand);
+ this.rebootCommand.RegisterCommand(this.RefreshDeviceNameCommand);
+ this.rebootCommand.RegisterCommand(this.StartListeningForSystemPerfCommand);
+ }
+
+ return this.rebootCommand;
+ }
+ }
+
+ ///
+ /// Predicate for determining whether the reboot command can execute
+ ///
+ /// Whether or not the reboot command can execute
+ private bool CanExecuteReboot()
+ {
+ return this.Ready;
+ }
+
+ ///
+ /// Performs the action of the reboot command
+ ///
+ /// A task that captures the continuation of the asynchronous reboot action
+ private async Task ExecuteRebootAsync()
+ {
+ this.OutputDiagnosticString("ExecuteRebootAsync\n");
+ this.Ready = false;
+ try
+ {
+ this.OutputDiagnosticString("Attempting to reboot device.\n");
+ await this.Portal.RebootAsync();
+
+ // Sometimes able to reestablish the connection prematurely before the console has a chance to shut down
+ // So adding a delay here before trying to reestablish the connection.
+ await Task.Delay(1000 * 5);
+ }
+ catch (Exception exn)
+ {
+ this.ReportException("Reboot", exn);
+ }
+
+ this.Ready = true;
+ }
+ #endregion // Reboot Command
+
+ #region RefreshConnectionCommand
+ ///
+ /// Command to refresh the connection with the remote device
+ ///
+ private CommandSequence refreshConnectionCommand;
+
+ ///
+ /// Gets the command that refreshes the connection with the remote device
+ ///
+ public ICommand RefreshConnectionCommand
+ {
+ get
+ {
+ if (this.refreshConnectionCommand == null)
+ {
+ this.refreshConnectionCommand = this.CreateCommandSequence();
+ this.refreshConnectionCommand.RegisterCommand(this.StopListeningForSystemPerfCommand);
+ this.refreshConnectionCommand.RegisterCommand(this.ReestablishConnectionCommand);
+ this.refreshConnectionCommand.RegisterCommand(this.RefreshDeviceNameCommand);
+ this.refreshConnectionCommand.RegisterCommand(this.StartListeningForSystemPerfCommand);
+ }
+
+ return this.refreshConnectionCommand;
+ }
+ }
+ #endregion // RefreshConnectionCommand
+
+ #region ReestablishConnectionCommand
+ ///
+ /// Command to reestablish the connection with the device
+ ///
+ private CommandSequence reestablishConnectionCommand;
+
+ ///
+ /// Gets the command for reestablishing the connection with the device
+ ///
+ public ICommand ReestablishConnectionCommand
+ {
+ get
+ {
+ if (this.reestablishConnectionCommand == null)
+ {
+ this.reestablishConnectionCommand = this.CreateCommandSequence();
+ DelegateCommand reestablishConnectionDC = DelegateCommand.FromAsyncHandler(this.ExecuteReestablishConnectionAsync, this.CanExecuteReestablishConnection);
+ reestablishConnectionDC.ObservesProperty(() => this.Ready);
+ this.reestablishConnectionCommand.RegisterCommand(reestablishConnectionDC);
+ }
+
+ return this.reestablishConnectionCommand;
+ }
+ }
+
+ ///
+ /// Predicate for the reestablish connection command
+ ///
+ /// Indicates whether the reestablish connection command can execute.
+ private bool CanExecuteReestablishConnection()
+ {
+ return this.Ready;
+ }
+
+ ///
+ /// Performs the action of the reestablish connection command
+ ///
+ /// A task capturing the continuation of the asynchronous action to reestablish the connection
+ private async Task ExecuteReestablishConnectionAsync()
+ {
+ this.OutputDiagnosticString("ExecuteReestablishConnectionAsync\n");
+ int numTries = 1;
+
+ DeviceConnectionStatus finalConnectionStatus = DeviceConnectionStatus.None;
+
+ DeviceConnectionStatusEventHandler handler = (DevicePortal sender, DeviceConnectionStatusEventArgs args) =>
+ {
+ this.OutputDiagnosticString("Connection status update: Status: {0}, Phase: {1}\n", args.Status, args.Phase);
+ if (args.Status == DeviceConnectionStatus.Connected)
+ {
+ this.OutputDiagnosticString("Connection succeeded after {0} tries.\n", numTries);
+ this.ConnectionStatus = DeviceConnectionStatus.Connected;
+ }
+ else if (args.Status == DeviceConnectionStatus.Failed)
+ {
+ this.OutputDiagnosticString("Connection failed after {0} tries.\n", numTries);
+ this.OutputDiagnosticString("HTTP Status: {0}\n", this.Portal.ConnectionHttpStatusCode);
+ this.OutputDiagnosticString("Failure description: {0}\n", args.Message);
+ }
+
+ finalConnectionStatus = args.Status;
+ };
+
+ this.Portal.ConnectionStatus += handler;
+
+ this.Ready = false;
+ try
+ {
+ do
+ {
+ await this.Portal.ConnectAsync();
+
+ if (this.Portal.ConnectionHttpStatusCode == HttpStatusCode.Unauthorized)
+ {
+ // Don't try to reconnect when there is an authentication failure
+ break;
+ }
+ else if (this.Portal.ConnectionHttpStatusCode != HttpStatusCode.OK && numTries <= this.ConnectionRetryAttempts)
+ {
+ await Task.Delay(1000 * 5);
+ }
+
+ ++numTries;
+ }
+ while (this.Portal.ConnectionHttpStatusCode != HttpStatusCode.OK && numTries <= this.ConnectionRetryAttempts);
+
+ if (this.Portal.ConnectionHttpStatusCode != HttpStatusCode.OK)
+ {
+ throw new Exception(string.Format("Unable to connect after {0} tries.", numTries - 1));
+ }
+
+ this.OnPropertyChanged("Address");
+ this.OnPropertyChanged("DeviceFamily");
+ this.OnPropertyChanged("OperatingSystemVersion");
+ this.OnPropertyChanged("Platform");
+ this.OnPropertyChanged("PlatformName");
+ }
+ catch (Exception exn)
+ {
+ this.ReportException("ReestablishConnection", exn);
+ }
+
+ this.Portal.ConnectionStatus -= handler;
+ this.Ready = true;
+ this.ConnectionStatus = finalConnectionStatus;
+ }
+ #endregion // ReestablishConnectionCommand
+
+ #region StartListeningForSystemPerfCommand
+ ///
+ /// Command to start listing for asynchronous system performance updates
+ ///
+ private CommandSequence startListeningForSystemPerfCommand;
+
+ ///
+ /// Gets the command to start listing for system performance updates
+ ///
+ public ICommand StartListeningForSystemPerfCommand
+ {
+ get
+ {
+ if (this.startListeningForSystemPerfCommand == null)
+ {
+ this.startListeningForSystemPerfCommand = this.CreateCommandSequence();
+ DelegateCommand startListeningForSystemPerfDC = DelegateCommand.FromAsyncHandler(this.ExecuteStartListeningForSystemPerfAsync, this.CanStartListeningForSystemPerf);
+ startListeningForSystemPerfDC.ObservesProperty(() => this.Ready);
+ this.startListeningForSystemPerfCommand.RegisterCommand(startListeningForSystemPerfDC);
+ }
+
+ return this.startListeningForSystemPerfCommand;
+ }
+ }
+
+ ///
+ /// Predicate for the command to start listening for system performance updates
+ ///
+ /// Result indicates whether the command can execute
+ private bool CanStartListeningForSystemPerf()
+ {
+ return this.Ready;
+ }
+
+ ///
+ /// Performs the action of the command to start listening for system performance updates
+ ///
+ /// A task that captures the continuation of the action to start listening for system performance updates
+ private async Task ExecuteStartListeningForSystemPerfAsync()
+ {
+ this.OutputDiagnosticString("ExecuteStartListeningForSystemPerfAsync\n");
+ this.Ready = false;
+ try
+ {
+ this.Portal.SystemPerfMessageReceived += this.OnSystemPerfReceived;
+ await this.Portal.StartListeningForSystemPerfAsync();
+ }
+ catch (Exception exn)
+ {
+ this.ReportException("StartListeningForSystemPerf", exn);
+ }
+
+ this.Ready = true;
+ }
+ #endregion // StartListeningForSystemPerfCommand
+
+ #region StopListeningForSystemPerfCommand
+ ///
+ /// Command to stop listing to asynchronous system performance updates
+ ///
+ private CommandSequence stopListeningForSystemPerfCommand;
+
+ ///
+ /// Gets the command to stop listening to asynchronous system performance updates
+ ///
+ public ICommand StopListeningForSystemPerfCommand
+ {
+ get
+ {
+ if (this.stopListeningForSystemPerfCommand == null)
+ {
+ this.stopListeningForSystemPerfCommand = this.CreateCommandSequence();
+ DelegateCommand stopListeningForSystemPerfDC = DelegateCommand.FromAsyncHandler(this.ExecuteStopListeningForSystemPerfAsync, this.CanStopListeningForSystemPerf);
+ stopListeningForSystemPerfDC.ObservesProperty(() => this.Ready);
+ this.stopListeningForSystemPerfCommand.RegisterCommand(stopListeningForSystemPerfDC);
+ }
+
+ return this.stopListeningForSystemPerfCommand;
+ }
+ }
+
+ ///
+ /// Predicate for the stop listening for system performance command
+ ///
+ /// Result indicates whether the command can execute
+ private bool CanStopListeningForSystemPerf()
+ {
+ return this.Ready;
+ }
+
+ ///
+ /// Performs the action of the command to stop listening to system performance updates
+ ///
+ /// Task the captures the continuation of the asynchronous action
+ private async Task ExecuteStopListeningForSystemPerfAsync()
+ {
+ this.OutputDiagnosticString("ExecuteStopListeningForSystemPerfAsync\n");
+ this.Ready = false;
+ try
+ {
+ this.Portal.SystemPerfMessageReceived -= this.OnSystemPerfReceived;
+ await this.Portal.StopListeningForSystemPerfAsync();
+ }
+ catch (Exception exn)
+ {
+ this.ReportException("StopListeningForSystemPerf", exn);
+ }
+
+ this.Ready = true;
+ }
+ #endregion // StopListeningForSystemPerfCommand
+ #endregion // Commands
+
+ ///
+ /// Event handler receives the system performance updates and sets the CPULoad property
+ ///
+ /// The DevicePortal that originated the event
+ /// The event data
+ private void OnSystemPerfReceived(DevicePortal sender, WebSocketMessageReceivedEventArgs args)
+ {
+ this.cpuLoad = args.Message.CpuLoad;
+ this.OnPropertyChanged("CPULoad");
+ }
+
+ ///
+ /// An SSL thumbprint that we'll accept.
+ ///
+ private string thumbprint;
+
+ ///
+ /// Validate the server certificate
+ ///
+ /// The sender object
+ /// The server's certificate
+ /// The cert chain
+ /// Policy Errors
+ /// whether the cert passes validation
+ private bool DoCertValidation(DevicePortal sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
+ {
+ X509Certificate2 cert = new X509Certificate2(certificate);
+
+ // If we have previously said to accept this cert, don't prompt again for this session.
+ if (!string.IsNullOrEmpty(this.thumbprint) && this.thumbprint.Equals(cert.Thumbprint, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ // We could alternatively ask the user if they wanted to always trust
+ // this device and we could persist the thumbprint in some way (registry, database, filesystem, etc).
+ MessageBoxResult result = MessageBox.Show(
+ string.Format(
+ "Do you want to accept the following certificate?\n\nThumbprint:\n {0}\nIssuer:\n {1}",
+ cert.Thumbprint,
+ cert.Issuer),
+ "Untrusted Certificate Detected",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question,
+ MessageBoxResult.No);
+
+ if (result == MessageBoxResult.Yes)
+ {
+ this.thumbprint = cert.Thumbprint;
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Samples/SampleDeviceCollection/ViewModels/DeviceSignInViewModel.cs b/Samples/SampleDeviceCollection/ViewModels/DeviceSignInViewModel.cs
new file mode 100644
index 00000000..23baa57b
--- /dev/null
+++ b/Samples/SampleDeviceCollection/ViewModels/DeviceSignInViewModel.cs
@@ -0,0 +1,476 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+
+using System;
+using System.Security;
+using System.Windows.Input;
+using Microsoft.Tools.WindowsDevicePortal;
+using Prism.Commands;
+using Prism.Mvvm;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// Enumerates the device families that may be selected by the user
+ ///
+ public enum DeviceFamilySelections
+ {
+ XboxOne,
+ HoloLens,
+ // TODO: Phone is not yet supported
+ // Phone,
+ IoT,
+ Desktop,
+ Other
+ }
+
+ ///
+ /// ViewModel for the device sign in flow
+ ///
+ public class DeviceSignInViewModel : BindableBase
+ {
+ //-------------------------------------------------------------------
+ // Private Members
+ //-------------------------------------------------------------------
+ #region Private Members
+ #endregion // Private Members
+
+ //-------------------------------------------------------------------
+ // Constructors
+ //-------------------------------------------------------------------
+ #region Constructors
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DeviceSignInViewModel()
+ {
+ this.DeviceFamily = DeviceFamilySelections.XboxOne;
+ this.ProtocolIsHttps = true;
+ }
+ #endregion // Constructors
+
+ //-------------------------------------------------------------------
+ // Properties
+ //-------------------------------------------------------------------
+ #region Properties
+ #region DeviceFamily
+ ///
+ /// The device family selected by the user
+ ///
+ private DeviceFamilySelections deviceFamily;
+
+ ///
+ /// Gets or sets the device family selected by the user
+ ///
+ public DeviceFamilySelections DeviceFamily
+ {
+ get
+ {
+ return this.deviceFamily;
+ }
+
+ set
+ {
+ this.SetProperty(ref this.deviceFamily, value);
+
+ // Prepopulate some "best guess" values for Desktop
+ if (this.DeviceFamily == DeviceFamilySelections.Desktop)
+ {
+ this.PrepopulateDesktopPort();
+ }
+
+ // All bets are off for "Other" so clear everything and leave it up to the user
+ if (this.DeviceFamily == DeviceFamilySelections.Other)
+ {
+ this.portUserEntry = string.Empty;
+ }
+
+ this.OnPropertyChanged("UsbAvailable");
+ this.OnPropertyChanged("UsbSelected");
+ this.OnPropertyChanged("ProtocolSelectionEnabled");
+ this.OnPropertyChanged("Port");
+ this.OnPropertyChanged("IsPortEntryEnabled");
+ this.OnPropertyChanged("AddressEntryEnabled");
+ this.OnPropertyChanged("Address");
+ if (this.DeviceFamily == DeviceFamilySelections.XboxOne)
+ {
+ this.ProtocolIsHttps = true;
+ }
+ }
+ }
+ #endregion // DeviceFamily
+
+ #region ProtocolIsHttps
+ ///
+ /// Bidning for a radio selection to disambiguate the URL protocol scheme for the user. Protocol can be either http or https.
+ ///
+ private bool protocolIsHttps;
+
+ ///
+ /// Gets or sets a value indicating whether the URL protocol scheme is https or http
+ ///
+ public bool ProtocolIsHttps
+ {
+ get
+ {
+ return this.protocolIsHttps;
+ }
+
+ set
+ {
+ this.SetProperty(ref this.protocolIsHttps, value);
+ if (this.DeviceFamily == DeviceFamilySelections.Desktop)
+ {
+ this.PrepopulateDesktopPort();
+ }
+
+ this.OnPropertyChanged("Port");
+ this.OnPropertyChanged("IsPortEntryEnabled");
+ this.OnPropertyChanged("Address");
+ }
+ }
+ #endregion // ProtocolIsHttps
+
+ #region ProtocolSelectionEnabled
+ ///
+ /// Gets a value indicating whether the user may select the URL protocol scheme
+ ///
+ public bool ProtocolSelectionEnabled
+ {
+ get
+ {
+ return this.DeviceFamily != DeviceFamilySelections.XboxOne;
+ }
+ }
+ #endregion // ProtocolSelectionEnabled
+
+ #region UsbAvailable
+ ///
+ /// Gets a value indicating whether USB selection is available
+ ///
+ public bool UsbAvailable
+ {
+ get
+ {
+ return this.DeviceFamily == DeviceFamilySelections.HoloLens;
+ // TODO: Phone is not yet supported
+ // || this.DeviceFamily == DeviceFamilySelections.Phone
+ }
+ }
+ #endregion // UsbAvailable
+
+ #region UsbSelected
+ ///
+ /// Binding property for a checkbox that indictates whether the user is attemptint to connect over USB
+ ///
+ private bool usbSelected;
+
+ ///
+ /// Gets or sets a value indicating whether the user is attempting to connect over USB
+ ///
+ public bool UsbSelected
+ {
+ get
+ {
+ return this.UsbAvailable && this.usbSelected;
+ }
+
+ set
+ {
+ this.SetProperty(ref this.usbSelected, value);
+ this.OnPropertyChanged("Port");
+ this.OnPropertyChanged("IsPortEntryEnabled");
+ this.OnPropertyChanged("Address");
+ this.OnPropertyChanged("AddressEntryEnabled");
+ }
+ }
+ #endregion // UsbSelected
+
+ #region IsPortEntryEnabled
+ ///
+ /// Gets a value indicating whether the user may enter the port value
+ ///
+ public bool IsPortEntryEnabled
+ {
+ get
+ {
+ switch (this.DeviceFamily)
+ {
+ case DeviceFamilySelections.HoloLens:
+ // TODO: Phone is not yet supported
+ // case DeviceFamilySelections.Phone:
+ case DeviceFamilySelections.XboxOne:
+ return false;
+
+ case DeviceFamilySelections.IoT:
+ return this.ProtocolIsHttps;
+
+ case DeviceFamilySelections.Other:
+ default:
+ return true;
+ }
+ }
+ }
+ #endregion // IsPortEntryEnabled
+
+ #region Port
+ ///
+ /// Binding property for a field where the user may enter a port value
+ ///
+ private string portUserEntry;
+
+ ///
+ /// Gets or sets the port value to use when connecting to the device.
+ ///
+ public string Port
+ {
+ get
+ {
+ string portForFamily = this.GetPortForDeviceFamily();
+ if (string.IsNullOrEmpty(portForFamily))
+ {
+ return this.portUserEntry;
+ }
+ else
+ {
+ return portForFamily;
+ }
+ }
+
+ set
+ {
+ this.SetProperty(ref this.portUserEntry, value);
+ }
+ }
+ #endregion // Port
+
+ #region GetPortForDeviceFamily
+ ///
+ /// Gets the port number to use based on the device family, protocol, and USB selections
+ ///
+ /// the port number as a string
+ private string GetPortForDeviceFamily()
+ {
+ switch (this.DeviceFamily)
+ {
+ case DeviceFamilySelections.HoloLens:
+ //case DeviceFamilySelections.Phone:
+ {
+ if (this.UsbSelected)
+ {
+ return "10080";
+ }
+
+ return this.ProtocolIsHttps ? "443" : "80";
+ }
+
+ case DeviceFamilySelections.IoT:
+ return this.ProtocolIsHttps ? string.Empty : "8080";
+ case DeviceFamilySelections.XboxOne:
+ return "11443";
+ case DeviceFamilySelections.Other:
+ default:
+ return string.Empty;
+ }
+ }
+ #endregion // GetPortForDeviceFamily
+
+ #region Address
+ ///
+ /// Binding property for a field where the user enters the device address
+ ///
+ private string addressUserEntry;
+
+ ///
+ /// Gets or sets a value containing the device address
+ ///
+ public string Address
+ {
+ get
+ {
+ if (this.UsbAvailable && this.UsbSelected)
+ {
+ return "localhost";
+ }
+
+ return this.addressUserEntry;
+ }
+
+ set
+ {
+ this.SetProperty(ref this.addressUserEntry, value);
+ }
+ }
+ #endregion // Address
+
+ #region AddressEntryEnabled
+ ///
+ /// Gets a value indicating whether the user may enter an address for the device.
+ /// (In case it is a USB connection then the address is always localhost.)
+ ///
+ public bool AddressEntryEnabled
+ {
+ get
+ {
+ return !this.UsbAvailable || !this.UsbSelected;
+ }
+ }
+ #endregion // AddressEntryEnabled
+
+ #region UserName
+ ///
+ /// Username to use when authentication with the device
+ ///
+ private string userName;
+
+ ///
+ /// Gets or sets the username to use when authenticating with the device
+ ///
+ public string UserName
+ {
+ get { return this.userName; }
+ set { this.SetProperty(ref this.userName, value); }
+ }
+ #endregion // UserName
+
+ #region Password
+ ///
+ /// Password for authenticating with the device
+ ///
+ private SecureString password;
+
+ ///
+ /// Gets or sets the password for authenticating with the device
+ ///
+ public SecureString Password
+ {
+ get { return this.password; }
+ set { this.SetProperty(ref this.password, value); }
+ }
+ #endregion // Password
+
+ ///
+ /// Prepopulate the port entry with a best guess value whenever the user selects Desktop
+ /// or when Desktop is already selected and the user changes the protocol
+ ///
+ private void PrepopulateDesktopPort()
+ {
+ if(this.ProtocolIsHttps)
+ {
+ // Some logic to prevent clobbering the user's entry
+ if(string.IsNullOrWhiteSpace(this.portUserEntry) || this.portUserEntry == "50080")
+ {
+ this.portUserEntry = "50443";
+ }
+ }
+ else
+ {
+ // Some logic to prevent clobbering the user's entry
+ if (string.IsNullOrWhiteSpace(this.portUserEntry) || this.portUserEntry == "50443")
+ {
+ this.portUserEntry = "50080";
+ }
+ }
+ }
+ #endregion // Properties
+
+ //-------------------------------------------------------------------
+ // Commands
+ //-------------------------------------------------------------------
+ #region Commands
+
+ #region Connect Command
+ ///
+ /// Command to connect to the device
+ ///
+ private DelegateCommand connectCommand;
+
+ ///
+ /// Gets the command for connecting with the device
+ ///
+ public ICommand ConnectCommand
+ {
+ get
+ {
+ if (this.connectCommand == null)
+ {
+ this.connectCommand = new DelegateCommand(this.ExecuteConnect, this.CanExecuteConnect);
+ this.connectCommand.ObservesProperty(() => this.Address);
+ this.connectCommand.ObservesProperty(() => this.Port);
+ this.connectCommand.ObservesProperty(() => this.UserName);
+ this.connectCommand.ObservesProperty(() => this.Password);
+ }
+
+ return this.connectCommand;
+ }
+ }
+
+ ///
+ /// Performs the action of the ConnectCommand
+ ///
+ private void ExecuteConnect()
+ {
+ string protocolAddressPort = string.Format(
+ @"{0}://{1}:{2}",
+ this.ProtocolIsHttps ? @"https" : @"http",
+ this.Address,
+ this.Port);
+
+ IDevicePortalConnection conn = new DefaultDevicePortalConnection(protocolAddressPort, this.UserName, this.Password);
+ this.SignInAttempted?.Invoke(this, new SignInAttemptEventArgs(conn));
+ }
+
+ ///
+ /// Predicate for the ConnectCommand
+ ///
+ /// Result indicates whether the ConnectCommand can execute
+ private bool CanExecuteConnect()
+ {
+ return
+ !string.IsNullOrWhiteSpace(this.Address) &&
+ !string.IsNullOrWhiteSpace(this.Port) &&
+ !string.IsNullOrWhiteSpace(this.UserName) &&
+ this.Password != null &&
+ this.Password.Length > 0;
+ }
+ #endregion // Connect Command
+ #endregion // Commands
+
+ #region Events
+ ///
+ /// Delegate describes a method for handling SignInAttempt events
+ ///
+ /// DeviceSignInViewModel that originated the event
+ /// Event arguments
+ public delegate void SignInAttemptEventHandler(DeviceSignInViewModel sender, SignInAttemptEventArgs args);
+
+ ///
+ /// The arguments for a DeviceSignInAttempt event
+ ///
+ public class SignInAttemptEventArgs : System.EventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The IDevicePortalConnection instance associated with the sign in attempt
+ internal SignInAttemptEventArgs(IDevicePortalConnection conn)
+ {
+ this.Connection = conn;
+ }
+
+ ///
+ /// Gets the IDevicePortalConnection instance associated with the sign in attempt
+ ///
+ public IDevicePortalConnection Connection { get; private set; }
+ }
+
+ ///
+ /// Event fires whenever the user tries to connect to another device.
+ ///
+ public event SignInAttemptEventHandler SignInAttempted;
+ #endregion // Events
+ }
+}
diff --git a/Samples/SampleDeviceCollection/ViewModels/DiagnosticOutputViewModel.cs b/Samples/SampleDeviceCollection/ViewModels/DiagnosticOutputViewModel.cs
new file mode 100644
index 00000000..dd50184e
--- /dev/null
+++ b/Samples/SampleDeviceCollection/ViewModels/DiagnosticOutputViewModel.cs
@@ -0,0 +1,89 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using Prism.Mvvm;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// View Model to provide diagnostic output to a view through the OutputStream string property
+ ///
+ public class DiagnosticOutputViewModel : BindableBase, IDiagnosticSink
+ {
+ //-------------------------------------------------------------------
+ // Private Members
+ //-------------------------------------------------------------------
+ #region Private Members
+ ///
+ /// The maximum number of characters to store before discarding diagnostic output
+ ///
+ private const int MaxBufferSize = 65535;
+ #endregion // Private Members
+
+ //-------------------------------------------------------------------
+ // Properties
+ //-------------------------------------------------------------------
+ #region Properties
+ #region OutputStream
+ ///
+ /// Stores the output that has already been written to the diagnostic sink
+ ///
+ private string outputStream;
+
+ ///
+ /// Gets a string that stors the output that has already been written to this diagnostic sink
+ ///
+ public string OutputStream
+ {
+ get
+ {
+ return this.outputStream;
+ }
+
+ private set
+ {
+ this.SetProperty(ref this.outputStream, value);
+ }
+ }
+ #endregion // OutputStream
+ #endregion // Properties
+
+ //-------------------------------------------------------------------
+ // IDiagnosticSink Implementation
+ //-------------------------------------------------------------------
+ #region IDiagnosticSink Implementation
+ ///
+ /// Prints a formatted diagnostic string to the output stream
+ ///
+ /// Format string
+ /// Format arguments
+ /// Automatically flushes the output
+ public void OutputDiagnosticString(string fmt, params object[] args)
+ {
+ this.outputStream += string.Format(fmt, args);
+ this.FlushOutput();
+ }
+ #endregion // IDiagnosticSink Implementation
+
+ //-------------------------------------------------------------------
+ // Private Methods
+ //-------------------------------------------------------------------
+ #region Private Methods
+ ///
+ /// Flush pending output
+ ///
+ private void FlushOutput()
+ {
+ int len = this.outputStream.Length;
+ if (len > MaxBufferSize)
+ {
+ this.outputStream = this.outputStream.Substring(len - MaxBufferSize);
+ }
+
+ this.OnPropertyChanged("OutputStream");
+ }
+ #endregion // Private Methods
+ }
+}
diff --git a/Samples/SampleDeviceCollection/ViewModels/DiagnosticSinks.cs b/Samples/SampleDeviceCollection/ViewModels/DiagnosticSinks.cs
new file mode 100644
index 00000000..fb7ce80b
--- /dev/null
+++ b/Samples/SampleDeviceCollection/ViewModels/DiagnosticSinks.cs
@@ -0,0 +1,110 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// Several IDiagnosticSink implementations
+ ///
+ public static class DiagnosticSinks
+ {
+ ///
+ /// Discards all diagnostic output
+ ///
+ public class NullDiagnosticSink : IDiagnosticSink
+ {
+ ///
+ /// Writes formatted output to the sink
+ ///
+ /// Format string
+ /// Format args
+ public void OutputDiagnosticString(string fmt, params object[] args)
+ {
+ }
+ }
+
+ ///
+ /// Sends diagnostic output to the console
+ ///
+ public class ConsoleDiagnositcSink : IDiagnosticSink
+ {
+ ///
+ /// Writes formatted output to the sink
+ ///
+ /// Format string
+ /// Format args
+ public void OutputDiagnosticString(string fmt, params object[] args)
+ {
+ Console.Write(string.Format(fmt, args));
+ }
+ }
+
+ ///
+ /// Sends diagnostic output to the debug channel (i.e. OutputDebugString)
+ ///
+ public class DebugDiagnosticSink : IDiagnosticSink
+ {
+ ///
+ /// Writes formatted output to the sink
+ ///
+ /// Format string
+ /// Format args
+ public void OutputDiagnosticString(string fmt, params object[] args)
+ {
+ Debug.Write(string.Format(fmt, args));
+ }
+ }
+
+ ///
+ /// Combines several diagnostic sinks together
+ ///
+ public class AggregateDiagnosticSink : IDiagnosticSink
+ {
+ ///
+ /// The diagnostic sinks that are aggregated together
+ ///
+ private IEnumerable sinks;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Collection of diagnostic sinks to aggregate together
+ public AggregateDiagnosticSink(params IDiagnosticSink[] args)
+ {
+ this.sinks = args;
+ }
+
+ ///
+ /// Writes formatted output to the sink
+ ///
+ /// Format string
+ /// Format args
+ public void OutputDiagnosticString(string fmt, params object[] args)
+ {
+ foreach (var diag in this.sinks)
+ {
+ diag.OutputDiagnosticString(fmt, args);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Interface describes a destination for diagnostic output
+ ///
+ public interface IDiagnosticSink
+ {
+ ///
+ /// Writes formatted output to the sink
+ ///
+ /// Format string
+ /// Format args
+ void OutputDiagnosticString(string fmt, params object[] args);
+ }
+}
diff --git a/Samples/SampleDeviceCollection/ViewModels/MainViewModel.cs b/Samples/SampleDeviceCollection/ViewModels/MainViewModel.cs
new file mode 100644
index 00000000..6b0a36b7
--- /dev/null
+++ b/Samples/SampleDeviceCollection/ViewModels/MainViewModel.cs
@@ -0,0 +1,537 @@
+//----------------------------------------------------------------------------------------------
+//
+// Licensed under the MIT License. See LICENSE.TXT in the project root license information.
+//
+//----------------------------------------------------------------------------------------------
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Net;
+using System.Windows;
+using System.Windows.Input;
+using Microsoft.Tools.WindowsDevicePortal;
+using Prism.Commands;
+using Prism.Mvvm;
+
+namespace SampleDeviceCollection
+{
+ ///
+ /// View model for the main view.
+ ///
+ public class MainViewModel : BindableBase
+ {
+ //-------------------------------------------------------------------
+ // Constructors
+ //-------------------------------------------------------------------
+ #region Constructors
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MainViewModel()
+ {
+ this.Diagnostics = new DiagnosticOutputViewModel();
+
+ DiagnosticSinks.DebugDiagnosticSink debugDiags = new DiagnosticSinks.DebugDiagnosticSink();
+ DiagnosticSinks.AggregateDiagnosticSink aggDiags = new DiagnosticSinks.AggregateDiagnosticSink(this.Diagnostics, debugDiags);
+
+ this.SignIn = new DeviceSignInViewModel();
+
+ this.SignIn.SignInAttempted += this.OnSignInAttemptCompleted;
+
+ this.ConnectedDevices.CollectionChanged += this.OnConnectedDevicesChanged;
+ }
+ #endregion // Constructors
+
+ ///
+ /// Handler for SignInAttempt events
+ ///
+ /// DeviceSignInViewModel originating the event
+ /// Arguments for the event
+ private void OnSignInAttemptCompleted(DeviceSignInViewModel sender, DeviceSignInViewModel.SignInAttemptEventArgs args)
+ {
+ this.ConnectedDevices.Add(new DevicePortalViewModel(args.Connection, this.Diagnostics));
+ }
+
+ //-------------------------------------------------------------------
+ // Properties
+ //-------------------------------------------------------------------
+ #region Properties
+ ///
+ /// Gets the diagnostic sink for this view model
+ ///
+ public DiagnosticOutputViewModel Diagnostics { get; private set; }
+
+ ///
+ /// Gets the ViewModel for the sign-in portion of the user experience
+ ///
+ public DeviceSignInViewModel SignIn { get; private set; }
+
+ #region Connected Devices
+ ///
+ /// Collection of view models for devices that have been connected to
+ ///
+ private ObservableCollection connectedDevices;
+
+ ///
+ /// Gets the collection of view models for the connected devices
+ ///
+ public ObservableCollection ConnectedDevices
+ {
+ get
+ {
+ if (this.connectedDevices == null)
+ {
+ this.connectedDevices = new ObservableCollection();
+ this.OnPropertyChanged("ConnectedDevices");
+ }
+
+ return this.connectedDevices;
+ }
+ }
+ #endregion // Connected Devices
+
+ #region SelectedDevices
+ ///
+ /// Subset of the collection of connected devices that have been selected by the user
+ ///
+ private IList selectedDevices;
+
+ ///
+ /// Gets or sets the subset of connected devices that have been selected by the user.
+ ///
+ public IList SelectedDevices
+ {
+ get
+ {
+ if (this.selectedDevices == null)
+ {
+ this.selectedDevices = new List();
+ }
+
+ return this.selectedDevices;
+ }
+
+ set
+ {
+ // This will be called when the contents of the list change without changing
+ // the actual list itself (i.e. same list, but different contents)
+ if (this.selectedDevices != value)
+ {
+ this.selectedDevices = value;
+ }
+
+ // We always want to fire the event to handle the case when contents change
+ this.OnPropertyChanged("SelectedDevices");
+ }
+ }
+ #endregion // SelectedDevices
+
+ #region SomeDevicesReady
+ ///
+ /// Gets a value indicating whether some of the devices in the ConnectedDevices collection are ready
+ ///
+ public bool SomeDevicesReady
+ {
+ get
+ {
+ foreach (DevicePortalViewModel dpvm in this.ConnectedDevices)
+ {
+ if (dpvm.Ready)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+ #endregion // SomeDevicesReady
+
+ #region AllDevicesReady
+ ///
+ /// Gets a value indicating whether all of the devices in the ConnectedDevices collection are ready
+ ///
+ public bool AllDevicesReady
+ {
+ get
+ {
+ foreach (DevicePortalViewModel dpvm in this.ConnectedDevices)
+ {
+ if (!dpvm.Ready)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+ #endregion // AllDevicesReady
+ #endregion // Properties
+
+ //-------------------------------------------------------------------
+ // Commands
+ //-------------------------------------------------------------------
+ #region Commands
+ #region SelectAllDevicesCommand
+ ///
+ /// Command to select all connected devices
+ ///
+ private DelegateCommand selectAllDevicesCommand;
+
+ ///
+ /// Gets the command to select all connected devices
+ ///
+ public ICommand SelectAllDevicesCommand
+ {
+ get
+ {
+ if (this.selectAllDevicesCommand == null)
+ {
+ this.selectAllDevicesCommand = new DelegateCommand(this.ExecuteSelectAllDevices, this.CanExecuteSelectAllDevices);
+ this.selectAllDevicesCommand.ObservesProperty(() => this.SomeDevicesReady);
+ this.selectAllDevicesCommand.ObservesProperty(() => this.SelectedDevices);
+ }
+
+ return this.selectAllDevicesCommand;
+ }
+ }
+
+ ///
+ /// Predicate for the SeletAllDevices command
+ ///
+ /// Result indicates whether the command can execute
+ private bool CanExecuteSelectAllDevices()
+ {
+ return this.SomeDevicesReady && (this.SelectedDevices.Count < this.ConnectedDevices.Count);
+ }
+
+ ///
+ /// Performs the action for the SelectAllDevices command
+ ///
+ private void ExecuteSelectAllDevices()
+ {
+ List newSelection = new List();
+ foreach (DevicePortalViewModel dpvm in this.ConnectedDevices)
+ {
+ if (dpvm.Ready)
+ {
+ newSelection.Add(dpvm);
+ }
+ }
+
+ this.SelectedDevices = newSelection;
+ }
+ #endregion // SelectAllDevicesCommand
+
+ #region UnSelectAllDevicesCommand
+ ///
+ /// Command to un-select all the devices
+ ///
+ private DelegateCommand unSelectAllDevicesCommand;
+
+ ///
+ /// Gets the command to un-select all the devices
+ ///
+ public ICommand UnSelectAllDevicesCommand
+ {
+ get
+ {
+ if (this.unSelectAllDevicesCommand == null)
+ {
+ this.unSelectAllDevicesCommand = new DelegateCommand(this.ExecuteUnSelectAllDevices, this.CanExecuteUnSelectAllDevices);
+ this.unSelectAllDevicesCommand.ObservesProperty(() => this.SelectedDevices);
+ }
+
+ return this.unSelectAllDevicesCommand;
+ }
+ }
+
+ ///
+ /// Predicate for the UnSelectAllDevices command
+ ///
+ /// Result indicates whether the command can excecute
+ private bool CanExecuteUnSelectAllDevices()
+ {
+ return this.SelectedDevices.Count > 0;
+ }
+
+ ///
+ /// Performs the action of the UnSelectAllDevices command
+ ///
+ private void ExecuteUnSelectAllDevices()
+ {
+ this.SelectedDevices = new List();
+ }
+ #endregion // UnSelectAllDevicesCommand
+
+ #region RebootSelectedDevicesCommand
+ ///
+ /// Command to reboot all the selected devices
+ ///
+ private DelegateCommand rebootSelectedDevicesCommand;
+
+ ///
+ /// Gets the command to reboot all the selected devices
+ ///
+ public ICommand RebootSelectedDevicesCommand
+ {
+ get
+ {
+ if (this.rebootSelectedDevicesCommand == null)
+ {
+ this.rebootSelectedDevicesCommand = new DelegateCommand(this.ExecuteRebootSelectedDevices, this.CanExecuteRebootSelectedDevices);
+ this.rebootSelectedDevicesCommand.ObservesProperty(() => this.SelectedDevices);
+ }
+
+ return this.rebootSelectedDevicesCommand;
+ }
+ }
+
+ ///
+ /// Predicate for the command to reboot all the devices
+ ///
+ /// Result indicates whether the command can execute.
+ private bool CanExecuteRebootSelectedDevices()
+ {
+ return this.SelectedDevices.Count > 0;
+ }
+
+ ///
+ /// Performs the action of the RebootSelectedDevices command
+ ///
+ private void ExecuteRebootSelectedDevices()
+ {
+ // When devices become busy, they will be reomoved from the selected devices collection.
+ // So, make a local copy of the devices we want to reboot:
+ List rebootList = new List();
+ foreach (DevicePortalViewModel dpvm in this.SelectedDevices)
+ {
+ if (dpvm.RebootCommand.CanExecute(this))
+ {
+ rebootList.Add(dpvm);
+ }
+ }
+
+ foreach (DevicePortalViewModel dpvm in rebootList)
+ {
+ dpvm.RebootCommand.Execute(this);
+ }
+ }
+ #endregion // RebootSelectedDevicesCommand
+
+ #region RemoveDeviceCommand
+ ///
+ /// Command to remove a device from the collection of connected devices
+ ///
+ private DelegateCommand