private static void OnNetworkTableChange(ITable table, string key, Value v, NotifyFlags flags) { try { //multiple objects could be bound to this key foreach (INotifyPropertyChanged source in propertyLookup.Keys) { OneToOneConversionMap <string, string> conversionMap = propertyLookup[source]; ITable boundTable = customTables[source]; object bindingSource = (source is DependencyNotifyListener) ? (object)(source as DependencyNotifyListener).source : source; if (table.ToString() != boundTable.ToString()) { continue; } if (conversionMap.TryGetBySecond(key, out string property)) { //the property that changed is bound to this object //grab the converter and use it if needed IValueConverter converter = conversionMap.GetConverterByFirst(property); PropertyInfo inf = bindingSource.GetType().GetProperty(property); //issue using v for some reason object value = NetworkUtil.ReadValue(boundTable.GetValue(key, null)); if (converter != null) { //in an NTConverter (required in API) the null values are never used so we don't need to set them object attemptedVal = converter.ConvertBack(value, null, null, null); //in case the conversion was invalid if (attemptedVal != DependencyProperty.UnsetValue) { value = attemptedVal; } } //correct any type inconsistencies (eg if we want to display an integer from the network, which only stores doubles) if (value != null && value.GetType() != inf.PropertyType) { Type targetType = inf.PropertyType; if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable <>)) { targetType = targetType.GetGenericArguments()[0]; } //anything still here can make an invalid cast to let them know to use a converter value = Convert.ChangeType(value, targetType); } //write to the object assignmentDispatch.Invoke(() => inf.SetValue(bindingSource, value)); } } } catch (InvalidOperationException e) { if (!e.Message.StartsWith("Collection was modified")) { throw e; } } }
/// <summary> /// Creates a binding between a dependency property and a network table entry /// </summary> /// <typeparam name="TLocal">The type of the entry in the dashboard</typeparam> /// <typeparam name="TNetwork">The type of the entry in the network table</typeparam> /// <param name="source">The object to bind to</param> /// <param name="property">The property to bind. Attached properties will throw an exception</param> /// <param name="networkPath">The full path of the entry in the network table</param> /// <param name="converter">The conversion mapping between the network values and local values</param> /// <param name="localOverride">Whether the local dashboard values should take precedence when the binding occurs</param> public static void Create <TLocal, TNetwork>(DependencyObject source, DependencyProperty property, string networkPath, NTConverter <TLocal, TNetwork> converter, bool localOverride = false) { if (!IsRunning) { throw new InvalidOperationException("Can only create bindings while the network table is running"); } Tuple <ITable, string> networkSource = NormalizeKey(networkPath); networkPath = networkSource.Item2; //because of additional work that needs to be done to bind the value, simpler to reimplement DependencyNotifyListener listener = new DependencyNotifyListener(source); if (!propertyLookup.ContainsKey(listener)) { propertyLookup[listener] = new OneToOneConversionMap <string, string>(); //this is a new item being bound, have it notify us of updates listener.PropertyChanged += OnLocalValueChange; } if (propertyLookup[listener].TryAdd(property.Name, networkPath)) { customTables[listener] = networkSource.Item1; //this means there were no binding conflicts //bind the dependency property to be notified of changes to it listener.BindProperty(property); //map a conversion if needed; null is valid propertyLookup[listener].MapConversionByFirst(property.Name, converter); //make the values consistent by forcing an update. //first check if the dashboard has a value at all Value data = networkSource.Item1.GetValue(networkPath, null); if (data == null || localOverride) { //send the local value to the network by "updating" the local value PropertyChangedEventArgs e = new PropertyChangedEventArgs(property.Name); OnLocalValueChange(listener, e); } else { //pull the dashboard from the local value by "updating" the network value OnNetworkTableChange(networkSource.Item1, networkPath, data, NotifyFlags.NotifyUpdate); } } }
/// <summary> /// Creates a binding between an observable object property and a network table entry of a different type /// </summary> /// <typeparam name="TLocal">The type of the entry in the dashboard</typeparam> /// <typeparam name="TNetwork">The type of the entry in the network table</typeparam> /// <param name="source">The object to bind to</param> /// <param name="property">The case-sensitive name of the property to bind</param> /// <param name="networkPath">The full path of the entry in the network table</param> /// <param name="converter">The conversion mapping between the network values and local values</param> /// <param name="localOverride">Whether the local dashboard values should take precedence when the binding occurs</param> public static void Create <TLocal, TNetwork>(INotifyPropertyChanged source, string property, string networkPath, NTConverter <TLocal, TNetwork> converter, bool localOverride = false) { if (!IsRunning) { throw new InvalidOperationException("Can only create bindings while the network table is running"); } Tuple <ITable, string> networkSource = NormalizeKey(networkPath); networkPath = networkSource.Item2; //add these to our dictionary if (!propertyLookup.ContainsKey(source)) { propertyLookup[source] = new OneToOneConversionMap <string, string>(); //this is a new item being bound; have it notify us of updates source.PropertyChanged += OnLocalValueChange; } if (propertyLookup[source].TryAdd(property, networkPath)) { customTables[source] = networkSource.Item1; //this means there were no binding conflicts //map a conversion if needed; null is a valid argument propertyLookup[source].MapConversionByFirst(property, converter); //make the values consistent by forcing an update. //first check if the dashboard has a value at all object data = networkSource.Item1.GetValue(networkPath, null); if (data == null || localOverride) { //send the local value to the network by "updating" the local value PropertyChangedEventArgs e = new PropertyChangedEventArgs(property); OnLocalValueChange(source, e); } else { //pull the dashboard from the local value by "updating" the network value OnNetworkTableChange(networkSource.Item1, networkPath, Value.MakeValue(data), NotifyFlags.NotifyUpdate); } } }
private static void OnLocalValueChange(object sender, PropertyChangedEventArgs e) { INotifyPropertyChanged obj = sender as INotifyPropertyChanged; OneToOneConversionMap <string, string> map = propertyLookup[obj]; ITable boundTable = customTables[obj]; object bindingSource = (obj is DependencyNotifyListener) ? (object)(obj as DependencyNotifyListener).source : obj; //first, verify that the property that changed is bound to something if (map.TryGetByFirst(e.PropertyName, out string networkPath)) { //grab the converter and use it if needed IValueConverter converter = map.GetConverterByFirst(e.PropertyName); PropertyInfo inf = bindingSource.GetType().GetProperty(e.PropertyName); object value = inf.GetValue(bindingSource); if (converter != null) { //in an NTConverter (required in API) the null values are never used so we don't need to set them object attemptedVal = converter.Convert(value, null, null, null); //in case the conversion was invalid if (attemptedVal != DependencyProperty.UnsetValue) { value = attemptedVal; } } //apparently network table STRONGLY dislikes null values if (value != null) { //write to the dashboard Value data = Value.MakeValue(value); boundTable.PutValue(networkPath, data); //the network table doesn't know any better and won't try to notify us of this change //therefore, bindings won't be updated again, so we should initiate a network table update //this is ok, because we only write to network table on effective value changes OnNetworkTableChange(boundTable, networkPath, data, NotifyFlags.NotifyUpdate); } } }