- Joined
- Nov 30, 2007
- Messages
- 1,202
This is a subscriber observable-interface you can use to manage all your callback methods to achieve data binding, should you need it. It essentially does what real variable change events does but for any data type.
Observers hold active Subscribers and data that can change. Whenever the value change the subscribers can be notified of updates that has occured.
LiveData is an extension of Observable and will automatically notify Subscribers whenever the data changes value. Note that assigning the same value does not trigger any callbacks.
Subscribers can observe one or more Observables and executes a callback whenever it is notified of state changes from the Observable.
The library includes an ArrayList that was used to reduce the code amount.
One of the strengths of this is that you can Create a Many-to-Many map, meaning: one Subscriber can watch many Observers, and equally one Observer can have many Subscribers.
I've provided a Lab to show how this library can be used:
Known Issues:
- LiveData does not work as intended when a struct extends it. The child struct has the same behavior as a regular Observable would (changes are not automatically notified).
Observers hold active Subscribers and data that can change. Whenever the value change the subscribers can be notified of updates that has occured.
LiveData is an extension of Observable and will automatically notify Subscribers whenever the data changes value. Note that assigning the same value does not trigger any callbacks.
Subscribers can observe one or more Observables and executes a callback whenever it is notified of state changes from the Observable.
The library includes an ArrayList that was used to reduce the code amount.
One of the strengths of this is that you can Create a Many-to-Many map, meaning: one Subscriber can watch many Observers, and equally one Observer can have many Subscribers.
JASS:
library Observables uses Table optional PArrayList
/*
===============================================================================================
Library: Observables
Version: 1.1
Made by: Pinzu
===============================================================================================
This library provides a subscriber observable interface for data binding.
The observable holds the data and can notify the subscriber of any changes. LiveData extends
Observable and will automatically notify the subscribers of any changes when its value change,
where as Observable requires the method notifyChanges() to be called. Note that the value must
actually be assigned to a different value for changes to be detected.
The subscriber can only observe one Observable at a time and is responsible for executing
the callback trigger when changes are detected.
===============================================================================================
Configuration
===============================================================================================
Simply add any data types you wish to include below:
*/
//! runtextmacro DEFINE_OBS("Integer", "integer", "0")
//! runtextmacro DEFINE_OBS("Boolean", "boolean", "false")
//! runtextmacro DEFINE_OBS("Real", "real", "0")
//! runtextmacro DEFINE_OBS("String", "string", "null")
//! textmacro_once DEFINE_OBS takes NAME, TYPE, NONE
/*
===============================================================================================
API
===============================================================================================
*** Access Globals ***
<NAME>Subscriber last<NAME>Sub Notified subscriber
Observable<NAME> lastObs<NAME> Changed observer
<TYPE> last<NAME>Value Changed value
*** struct <NAME>Subscriber ***
method operator callback= takes code c returns nothing
Changes the callback function to the provided code
method subscribe takes Observable<NAME> observer returns nothing
Adds an observable to the subscription.
method unsubscribe takes Observable<NAME> observer returns nothing
Removes the observable from the subscription
method clear takes nothing returns nothing
Stops subscribing to all observables
method destroy takes nothing returns nothing
Destroys it
static method create takes code callback returns thistype
Creates it
*** struct Observable<NAME> ***
method operator value= takes <TYPE> newValue returns nothing
Sets the value of the Observable
method operator value takes nothing returns <TYPE>
Gets the value of the Observable
method hasChanged takes nothing returns boolean
Returns true if the assigned value differs from the previous one
method notifyChanges takes nothing returns nothing
Notifies all current subscribers that changes have been made.
method hasSubscriber takes <NAME>Subscriber subscriber returns boolean
Returns true if the Subscriber is observing the Observable.
method subscribe takes <NAME>Subscriber subscriber returns nothing
Adds a Subscriber to the subscription
method unsubscribe takes <NAME>Subscriber subscriber returns nothing
Removes a Subscriber from the subscription
method clear takes nothing returns nothing
Removes all Subscribers from the subscription.
method destroy takes nothing returns nothing
Destroys it
static method create takes <TYPE> newValue returns thistype
Creates it with the assigned value.
LiveData has the same methods as Observable, the only difference in behavior is that LiveData will
automatically notify Subscribers whenever the value changes.
*** struct <NAME>LiveData extends Observable<NAME> ***
*/
globals
$NAME$Subscriber last$NAME$Sub
Observable$NAME$ lastObs$NAME$
$TYPE$ last$NAME$Value
endglobals
struct $NAME$Subscriber
private trigger t
private IntArrayList observables
method operator callback= takes code c returns nothing
call TriggerClearConditions(.t)
if not (c == null) then
call TriggerAddCondition(this.t, Condition(c))
endif
endmethod
// Should not be used outside this library scope
method observers takes nothing returns IntArrayList
return observables
endmethod
static method create takes code callback returns thistype
local thistype this = .allocate()
set this.observables = IntArrayList.create()
set this.t = CreateTrigger()
call TriggerAddCondition(this.t, Condition(callback))
return this
endmethod
method destroy takes nothing returns nothing
call .clear()
call .observables.destroy()
call TriggerClearConditions(.t)
call DestroyTrigger(.t)
set .t = null
endmethod
// Should not be used outside this library scope
method onChange takes nothing returns nothing
set last$NAME$Sub = this
call TriggerEvaluate(.t)
endmethod
method subscribe takes Observable$NAME$ observer returns nothing
call observer.subscribe(this)
endmethod
method unsubscribe takes Observable$NAME$ observer returns nothing
call observer.unsubscribe(this)
endmethod
method clear takes nothing returns nothing
local integer i = .observables.length -1
loop
exitwhen i < 0
call Observable$NAME$(.observables[i]).unsubscribe(this)
set i = i - 1
endloop
endmethod
endstruct
struct Observable$NAME$
method operator value= takes $TYPE$ newValue returns nothing
local boolean valueDiff = Table(this).$TYPE$[-1] != newValue
set Table(this).$TYPE$[-1] = newValue
set Table(this).boolean[-2] = valueDiff
if valueDiff and .getType() == $NAME$LiveData.typeid then
call .notifyChanges()
endif
endmethod
method operator value takes nothing returns $TYPE$
return Table(this).$TYPE$[-1]
endmethod
private method operator subscribers takes nothing returns IntArrayList
return Table(this)[-3]
endmethod
method hasChanged takes nothing returns boolean
return Table(this).boolean[-2]
endmethod
static method create takes $TYPE$ newValue returns thistype
local Observable$NAME$ this = Table.create()
set Table(this)[-3] = IntArrayList.create()
set this.value = newValue
return this
endmethod
method destroy takes nothing returns nothing
call .clear()
call .subscribers.destroy()
call Table(this).destroy()
endmethod
method clear takes nothing returns nothing
local integer i = .subscribers.length - 1
loop
exitwhen i < 0
call .unsubscribe(.subscribers[i])
set i = i - 1
endloop
endmethod
method notifyChanges takes nothing returns nothing
local integer i = 0
set lastObs$NAME$ = this
set last$NAME$Value = .value
loop
exitwhen i == .subscribers.length
call $NAME$Subscriber(.subscribers[i]).onChange()
set i = i + 1
endloop
endmethod
method hasSubscriber takes $NAME$Subscriber subscriber returns boolean
return .subscribers.contains(subscriber)
endmethod
method subscribe takes $NAME$Subscriber subscriber returns nothing
if hasSubscriber(subscriber) then
return
endif
call .subscribers.add(0, subscriber)
call subscriber.observers().add(0, this)
endmethod
method unsubscribe takes $NAME$Subscriber subscriber returns nothing
local integer index = .subscribers.indexOf(subscriber, true)
local IntArrayList subObservers
if index != -1 then
call subscribers.remove(index)
set subObservers = subscriber.observers()
call subObservers.remove(subObservers.indexOf(this, true))
endif
endmethod
endstruct
struct $NAME$LiveData extends Observable$NAME$
/* This is empty due to the difference in behavior being managed inside the parent instead. */
endstruct
//! endtextmacro
/*
===============================================================================================
Injected library if the map does not already have it
===============================================================================================
*/
// library PArrayList uses Table
/*
Made by: Pinzu
This is a very basic list using Table for storage.
Requirements:
Table by Bribe
hiveworkshop.com/threads/snippet-new-table.188084/
*/
static if not LIBRARY_PArrayList then
//! runtextmacro DEFINE_ARRAY_LIST("Int", "integer", "0")
//! textmacro_once DEFINE_ARRAY_LIST takes NAME, TYPE, NONE
struct $NAME$ArrayList
static method create takes nothing returns thistype
local thistype this = Table.create()
set this.length = 0
return this
endmethod
implement optional $NAME$ArrayListSortModule
method destroy takes nothing returns nothing
call Table(this).destroy()
endmethod
method clear takes nothing returns nothing
call Table(this).flush()
set .length = 0
endmethod
private method operator length= takes integer index returns nothing
set Table(this).integer[-1] = index
endmethod
method operator length takes nothing returns integer
return Table(this).integer[-1]
endmethod
// Getter: list[index]
method operator [] takes integer index returns $TYPE$
if (index < 0 or index >= .length) then
debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, "$NAME$ArrayList.get:IndexOutOfBounds")
return $NONE$
endif
return Table(this).$TYPE$[index]
endmethod
// Setter: list[index] = element
method operator []= takes integer index, $TYPE$ element returns nothing
if (index < 0 or index >= .length) then
debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, "$NAME$ArrayList.set:IndexOutOfBounds")
return
endif
set Table(this)[index] = element
endmethod
method add takes integer index, $TYPE$ element returns boolean
local integer i
if (index < 0 or index > .length) then
debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, "$NAME$ArrayList.add:IndexOutOfBounds")
return false
endif
set i = .length
loop
exitwhen i == index
set Table(this).$TYPE$[i] = Table(this).$TYPE$[i - 1]
set i = i - 1
endloop
set Table(this).$TYPE$[index] = element
set .length = .length + 1
return true
endmethod
method remove takes integer index returns $TYPE$
local integer i
local $TYPE$ returnValue
if (index < 0 or index >= .length) then
debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, "$NAME$ArrayList.remove:IndexOutOfBounds")
return $NONE$
endif
set i = index
set .length = .length - 1
loop
exitwhen i == .length
set Table(this).$TYPE$[i] = Table(this).$TYPE$[i + 1]
set i = i + 1
endloop
set returnValue = Table(this)[i]
call Table(this).$TYPE$.remove(.length)
return returnValue
endmethod
method contains takes $TYPE$ value returns boolean
return .indexOf(value, true) != -1
endmethod
method indexOf takes $TYPE$ value, boolean ascending returns integer
local integer i
if ascending then
set i = 0
loop
exitwhen i == .length
if Table(this).$TYPE$[i] == value then
return i
endif
set i = i + 1
endloop
else
set i = .length - 1
loop
exitwhen i < 0
if Table(this).$TYPE$[i] == value then
return i
endif
set i = i - 1
endloop
endif
return -1
endmethod
method isEmpty takes nothing returns boolean
return .length == 0
endmethod
method size takes nothing returns integer
return .length
endmethod
method swap takes integer indexA, integer indexB returns nothing
local $TYPE$ temp
if (indexA < 0 or indexA >= .length or indexB < 0 or indexB >= .length) then
debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, "$NAME$ArrayList.swap:IndexOutOfBounds")
return
endif
set temp = Table(this).$TYPE$[indexA]
set Table(this).$TYPE$[indexA] = Table(this).$TYPE$[indexB]
set Table(this).$TYPE$[indexB] = temp
endmethod
static method copy takes $NAME$ArrayList src, integer srcPos, $NAME$ArrayList dest, integer destPos, integer range returns nothing
local integer i = 0
if srcPos < 0 or srcPos == src.length or destPos < 0 or destPos > dest.length then
debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, "$NAME$ArrayList.copy:IndexOutOfBounds")
return
endif
loop
exitwhen i == range or srcPos >= src.length
call dest.add(destPos, src[srcPos])
set destPos = destPos + 1
set srcPos = srcPos + 1
set i = i + 1
endloop
endmethod
debug method print takes nothing returns nothing
debug local integer i = 0
debug local string s = "[" + I2S(.length) + "]: "
debug loop
debug exitwhen i == .length
debug set s = s + I2S(this[i]) + ", "
debug set i = i + 1
debug endloop
debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, s)
debug endmethod
endstruct
//! endtextmacro
endif
// endlibrary PArrayList uses Table
endlibrary
I've provided a Lab to show how this library can be used:
JASS:
scope Example1
private function OnChange takes nothing returns nothing
// These are the globals available to us on callback
call BJDebugMsg("Notified Subscriber: " + I2S(lastIntegerSub)) // Notified subscriber
call BJDebugMsg("Changed Observable: " + I2S(lastObsInteger)) // Changed Observable
call BJDebugMsg("Changed Value: " + I2S(lastIntegerValue)) // Observable Value
endfunction
function RunEx1 takes nothing returns nothing
// Here we create an observable integer with value 0 and then we bind it to our subscriber
local ObservableInteger obs = ObservableInteger.create(0)
// Here we are creating a subscriber with a callback function
local IntegerSubscriber sub = IntegerSubscriber.create(function OnChange)
// We can always change the callback function
set sub.callback = function OnChange
// Here we bind the observer to the subscriber
call sub.subscribe(obs)
call BJDebugMsg("Ex 1")
set obs.value = 10
// This is required to execute the callback, without it nothing happens
call obs.notifyChanges()
endfunction
endscope
scope Example2
// This works exactly as before
private function OnChange takes nothing returns nothing
call BJDebugMsg("Changed Value: " + I2S(lastIntegerValue))
endfunction
function RunEx2 takes nothing returns nothing
// Now lets try using live data instead, and this time create 2!
local IntegerLiveData liveData1 = IntegerLiveData.create(5)
local IntegerLiveData liveData2 = IntegerLiveData.create(10)
// Again we subscribe to the LiveData the same way as we did before
local IntegerSubscriber sub = IntegerSubscriber.create(function OnChange)
call sub.subscribe(liveData1)
call sub.subscribe(liveData2)
call BJDebugMsg("Ex 2")
// The difference between LiveData and observables is that they will automatically
// trigger a callback each time their value change
set liveData1.value = 100 // this will trigger a callback
set liveData1.value = 100 // this will not, as the value is unchanged
set liveData2.value = 900 // We can watch multiple observers from the same subscriber
endfunction
endscope
scope Example3
private function OnChange takes nothing returns nothing
call BJDebugMsg("Changed Value: " + I2S(lastIntegerValue))
endfunction
function RunEx3 takes nothing returns nothing
local IntegerLiveData liveData = IntegerLiveData.create(19)
local IntegerSubscriber sub = IntegerSubscriber.create(function OnChange)
// You can also subscribe using the observer directly
call liveData.subscribe(sub)
call BJDebugMsg("Ex 3")
set liveData.value = liveData.value *100 // Will trigger a callback
endfunction
endscope
scope Example4
// We can also extend Observables (Live Data behavior can not be extended however)
struct Talker extends ObservableInteger
private string str
static method create takes nothing returns thistype
local thistype this = .allocate(0) // We have now created an Observable with value 0
return this
endmethod
method message takes string s returns nothing
set this.str = s
set .value = GetRandomInt(0, 100)
call this.notifyChanges()
endmethod
// A new message was received, display it!
method display takes nothing returns nothing
call BJDebugMsg(this.str + " " + I2S(lastIntegerValue))
endmethod
endstruct
private function OnChange takes nothing returns nothing
call Talker(lastObsInteger).display()
endfunction
function RunEx4 takes nothing returns nothing
local Talker talker = Talker.create()
local IntegerSubscriber sub = IntegerSubscriber.create(function OnChange)
call sub.subscribe(talker)
call BJDebugMsg("Ex 4")
call talker.message("Hello!")
endfunction
endscope
// This example didnt turn out as well as I hoped, but my point is that you can create funny design :d
scope Example5
struct Sender
ObservableString msg
ObservableString error
static method create takes nothing returns thistype
local thistype this = .allocate()
set this.msg = ObservableString.create("")
set this.error = ObservableString.create("")
return this
endmethod
method work takes nothing returns nothing
local integer rdm = GetRandomInt(0, 100)
if rdm < 50 then
set msg.value = "We have a random number: " + I2S(rdm) + "!"
call msg.notifyChanges()
else
set error.value = "We failed to get a random number :("
call error.notifyChanges()
endif
endmethod
endstruct
struct Reciever extends StringSubscriber
private static method manageChanges takes nothing returns nothing
call BJDebugMsg("Msg recieved: " + lastStringValue)
endmethod
static method create takes ObservableString o1, ObservableString o2 returns thistype
local thistype this = .allocate(function thistype.manageChanges)
call this.subscribe(o1)
call this.subscribe(o2)
return this
endmethod
endstruct
function RunEx5 takes nothing returns nothing
local Sender sender = Sender.create()
local Reciever reciever = Reciever.create(sender.msg, sender.error)
call BJDebugMsg("Ex 5")
call sender.work()
endfunction
endscope
scope TestObserver initializer Init
private function Init takes nothing returns nothing
call ExecuteFunc("RunEx1")
call ExecuteFunc("RunEx2")
call ExecuteFunc("RunEx3")
call ExecuteFunc("RunEx4")
call ExecuteFunc("RunEx5")
endfunction
endscope
Known Issues:
- LiveData does not work as intended when a struct extends it. The child struct has the same behavior as a regular Observable would (changes are not automatically notified).
Last edited: